Repository navigation
CWG2985 [dcl.init.ref] Direct-initialization of a reference after conversion #596
Description
Activity
- changed the title
[-][dcl.init.ref] Specify which form of direct initialization that is used to direct initialize the reference and also make reference initialization more consistent[/-][+][dcl.init.ref] Specify which form of direct initialization is used to direct initialize the reference and also make reference initialization more consistent[/+]on Aug 13, 2024 I retracted my previous comment.
I don't know if we want to set a precedent of saying "direct-non-list-initialization" everywhere we mean that, because there are probably other places that would need to be changed. Instead maybe we should say somewhere that to direct-initialize an object or reference from an expression E means to initialize it as if its initializer were
(E).Reacted by S. B. Tam and Jens MaurerFor this particular example, I don't know if it matters whether direct-list-initialization is used to initialize the reference in the last step. If you used direct-list-initialization, you would fall through to [dcl.init.list]/3.9 which just tells you to use direct-initialization anyway.
How about in this example:
struct S { operator double(); }; int&& r{S()};Here, the choice of whether to use list-initialization for the last step affects whether it's well-formed, because the conversion from
doubletointwould be narrowing in a list-initialization context.EDG and GCC accept it. Clang and MSVC reject.
This example very much looks like a narrowing situation to me. It should be ill-formed.
@jensmaurer Are you referring to
#1or#2or both. I think both should be ill-formed. Note that to clarify that#1is ill-formed we will need to specify which form of direct initialization(( )or{ }) is used even though they might both eventually lead to the same conclusion. The second issue is that the#2should also be ill-formed(as you said, assuming you were referring to#2) but it is not clear if#2is ill-formed.@t3nsor Yes, the other option/alternative is to specify directly the
(E)form of direct initialization as mentioned in the original issue without theE.I'm referring to @t3nsor's latest example:
struct S { operator double(); }; int&& r{S()};@jensmaurer Yes, that is why I included it in the new issue. I agree that it should also be ill-formed.
What does the current wording say about
#2? Is that well-formed?@jensmaurer
#2which is (X&& ref{z};) seems to be well-formed as explained below.First
X&& r{z};is list-initialization. So we reach dcl.init.list#3:List-initialization of an object or reference of type cv T is defined as follows:
- [..]
- [...]
- Otherwise, if T is a reference type, a prvalue is generated. The prvalue initializes its result object by copy-list-initialization from the initializer list. The prvalue is then used to direct-initialize the reference.
The type of the prvalue is the type referenced by T, unless T is “reference to array of unknown bound of U”, in which case the type of the prvalue is the type of x in the declaration U x[] H, where H is the initializer list.
So first we need to check if the copy-list-initialization of the result object from the initializer list
{z}. Basically, we need to check the validity ofX resultObject = {z};to move further.So we go to dcl.init#general-16:
The semantics of initializers are as follows.
The destination type is the cv-unqualified type of the object or reference being initialized and the source type is the type of the initializer expression.
If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.- If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized ([dcl.init.list]).
So we again move to dcl.init.list, this time to check the validity of
X resultObject = {z};:List-initialization of an object or reference of type cv T is defined as follows:
- [...]
- [...]
- Otherwise, if T is a class type, constructors are considered.
The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]).
If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
This means that
X's constructors(including the implicit copy ctor etc) will be enumerated for the argumentz. Now we move to over.match:Overload resolution selects the function to call in seven distinct contexts within the language:
- [...]
- [...]
- invocation of a user-defined conversion for copy-initialization of a class object ([over.match.copy]);
Now, we go to over.match#copy:
Under the conditions specified in [dcl.init], as part of a copy-initialization of an object of class type, a user-defined conversion can be invoked to convert an initializer expression to the type of the object being initialized. Overload resolution is used to select the user-defined conversion to be invoked.
Assuming that “cv1 T” is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:
- [...]
- When the type of the initializer expression is a class type “cv S”, conversion functions are considered. The permissible types for non-explicit conversion functions are T and any class derived from T.
When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to cv2 T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv3 T”, the permissible types for explicit conversion functions are the same; otherwise there are none.
In both cases, the argument list has one argument, which is the initializer expression.
This means that the conversion function
Y::operator const Y () constcan be used to convert the initializer expressionzto an rvalue which will then be used as argument for one ofX's ctors.The important thing is that only the implicit copy ctor
X::X(const X&&)is viable here for the rvalue obtained as a resut of the conversion function.The implicit move ctorX::X(X&&)is not viable because it's parameter is a non-const rvalue reference.
Note that if you change the move ctor's parameter to be
const X&&(demo), then both the copy ctor and the move ctor will be viable for the rvalue obtaied from the conversion operator but then move ctorX::X(const X&&)will be a better match than the copy ctorX::X(const X&)because the argument(result of conversion operator) is a rvalue.The cross-reference in dcl.init.list points to [over.match.list], so I'd go there, not to over.match.copy.
@jensmaurer We would've to take both [over.match] as well as [over.match.list] into account as dcl.init#list-3.7(which was applied here), cross references both of them(over.match and over.match.list). Btw I only omitted writing [over.match.list] explicitly in my previous comment as even after [over.match.list] we'd still have to take [over.match] into account.
The list you quoted from [over.match] is introductory fluff; it should never be used to select a subclause of [over.match]. Again, please use [over.match.list] in your analysis, because that's what the normative intent is. We always use the rules for list-initialization overload resolution when an initializer list is involved.
@jensmaurer I've already used over.match.list in my analysis. I've just not written it explicitly. In particular, [over.match.list] says:
When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:
- [...]
- Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.
Note the emphasis on "the argument list consists of elements of initializer list" which here will mean that the argument list consists of
zwhich is exactly why in my original analysis I wrote:This means that
X's constructors(including the implicit copy ctor etc) will be enumerated for the argumentz.I though it was clear/trivial that now
zwill be the argument so I omitted quoting that [over.match.list] explicitly but used it in the analysis. Now, it should be clear whyzwas taken as argument there.Anyways, I've now added a link there in the original comment so that future readers can see why
zwas used as argument.@jensmaurer There also seems to be a clang bug reported for this last year. They mention different behavior there for different c++ versions. The example there is almost same as ours
#2.I think your analysis is correct. All implementations do accept
X x = {z};and so [dcl.init.list]/3.10 effectively tells us to bind the reference to a temporary that is copy-initialized in the same manner as that hypotheticalx.Basically every time you add a surrounding pair of braces, you enable one additional user-defined function to be called. For example
X x = {{z}};could call 3 user-defined conversions in sequence to convertztoX.With
// #1, you only get to call one user-defined function and the reference must bind to the result, so it's ill-formed.The interesting thing about
// #1is that it used to be well-formed before CWG1604 changed the rule from "bind to a copy-initialized temporary" to "bind to the result of the conversion". The purpose of CWG1604 was to eliminate an unnecessary additional copy in the era before guaranteed copy elision. I think we could probably make a useful tweak to the rules here: when you call a conversion function that returns a prvalue in [dcl.init.ref]/5.4.1, adjust the prvalue's cv-qualification to match that of the referenced type.There's also the interesting fact that direct-initialization of a reference to
T(using parentheses) never uses the rules for direct-initialization of aTobject. So, whileX x(z);is well-formed (though Clang rejects it, as reported in the bug linked above) the same is not true ofX&& x(z). This curiosity was noted during discussion of CWG2709. I don't know why it's like that, but it is what it is. Changing it would require a paper and implementation experience.Oh yes, you're right. But all compilers accept
const X& r = z, so there is no rationale for Clang to rejectX x = {z};.No, wait, I see what the problem is now. It's similar to the issue Richard pointed out: Clang doesn't like
X x = {z};because it thinks the move constructor is also viable. In other words Clang considers there to be an implicit conversion sequence fromztoX&&even though the corresponding copy-initializationX&& r = z;would be ill-formed.I also thought this but then looked at the clang's error which suggests that it doesn't consider the move ctor viable as the clang error says:
note: candidate constructor not viable: no known conversion from 'Z' to 'X &&' for 1st argument
7 | X(X&&);
| ^ ~~~Also, it says same for copy ctor not being viable. If the error were to say something like ambiguous conversion then we could've said that clang also considers move ctor to be viable. Maybe the error message is also faulty.
Note that if we explicitly declare/define the copy ctor without declaring/defining the move ctor then clang starts accepting it as well. Demo
struct X { public: X(){} X(const X&) //clang starts accepting this { }; }; X x = {z}; //now clang accepts thisI'm going to assume that's just a problem with Clang's diagnostics.
So, anyway, the issue of whether there's an implicit conversion sequence from
ztoX&&is a Clang bug. As Richard points out, the standard is not incredibly clear, but on the other hand, he also says: "It seems pretty clear to me that the move constructor should not be viable in this example." Really, the wording issue here is just a particular case of [CWG2525].Going back to
X&& r{z}, that means it should be accepted, Clang only rejects it because of the aforementioned bug.So we only have two new issues here:
- Do we want to say "direct-non-list-initialize" or do we want to say
(E)or whatever... - Do we want to reverse a possibly unintentional effect of CWG1604 as mentioned here CWG2985 [dcl.init.ref] Direct-initialization of a reference after conversion #596 (comment)
- Do we want to say "direct-non-list-initialize" or do we want to say
Jens, can we have a core issue for this please?
- changed the title
[-][dcl.init.ref] Specify which form of direct initialization is used to direct initialize the reference and also make reference initialization more consistent[/-][+]CWG2985 [dcl.init.ref] Direct-initialization of a reference after conversion[/+]on Jan 19, 2025
Full name of submitter: Anoop Rana
Reference (section label): [dcl.init.ref]
Link to reflector thread (if any): https://stackoverflow.com/questions/78867403/reference-initialization-using-list-initialization-accepted-by-gcc-and-msvc-but
Issue description:
There are two main issues described here. First is that dcl.init.ref#5.4.1.sentence-2 doesn't specify if the reference is direct initialized using
()or using{}. This matters becauseX&& ref{z};form is accepted by three compilers whileX&& r = z;is rejected by all. The second issue is that we should makeX&& r =z;andX&&{z};be more consistent and ill-formed.So to make the issue clearer, consider the following example: Demo
First we see how/why
#1is almost ill-formed as per the current wording. I say "almost" because [dcl.init.ref#5.4.1] doesn't specify if()or{}form is used for the direct initialization. And this might make a difference as already seen fromX&& ref{z};which is accepted by 3 compilers but not clang.So to understand
#1we go to dcl.init.ref:This means that the result of the conversion function which will be of type
const Ywill be used to direct initializer.So the initialization process will be repeated again. This time we first go to direct initialization:
So now come to dcl.init.ref and see that none of the bullet points is applicable for initialization of the reference
rfromconst Y.In particular, 5.1 is not applicable because the reference is not lvalue reference.
Similarly 5.2 is not applicable.
5.3 is also not applicable.
5.4 is not applicable because
const Yis reference related toXasXis a base.So none of the bullet points are applicable for the direct initialization of
rbyconst Yand soX&& r = z;is ill-formed.The important thing here is that here we didn't go to dcl.init#general-16.1 but instead to dcl.init#general-16.2. But the dcl.init.ref#5.4.1.sentence-2 currently doesn't specify that
{}form of direct initialization is not used.We can solve this first issue by changing "direct initialization" to "direct-non-list-initialization" or directly saying
()form is used instead of{}.Additionally, there might also be another issue(that of consistency) between not allowing
X&& r = z;but allowingX&& ref{z};. That should also be fixed(if any) by additional wording changes(apart from what is suggested here).Suggested Resolution
Change dcl.init.ref#5.4.1.sentence-2 to as highlighted below in bold: