Repository navigation
CWG2985 [dcl.init.ref] Direct-initialization of a reference after conversion #596
Description
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 because X&& ref{z}; form is accepted by three compilers while X&& r = z; is rejected by all. The second issue is that we should make X&& r =z; and X&&{z}; be more consistent and ill-formed.
So to make the issue clearer, consider the following example: Demo
struct X
{
public:
X(){}
};
struct Y : X
{
};
struct Z
{
public:
operator const Y () const
{
return {};
}
};
int main()
{
Z z;
// X&& r = z; // #1: All four rejects this
X&& ref{z}; //#2: Clang: Nope, gcc: Ok, msvc: Ok, Edg: Ok
}
First we see how/why #1 is 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 from X&& ref{z}; which is accepted by 3 compilers but not clang.
So to understand #1 we go to dcl.init.ref:
A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:
- [...]
- [...]
- [...]
- Otherwise, T1 shall not be reference-related to T2.
- If T1 or T2 is a class type, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion ([dcl.init], [over.match.copy], [over.match.conv]); the program is ill-formed if the corresponding non-reference copy-initialization would be ill-formed.
The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference.
For this direct-initialization, user-defined conversions are not considered.
This means that the result of the conversion function which will be of type const Y will be used to direct initialize r.
So the initialization process will be repeated again. This time we first go to direct initialization:
If the destination type is a reference type, see [dcl.init.ref].
So now come to dcl.init.ref and see that none of the bullet points is applicable for initialization of the reference r from const 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 Y is reference related to X as X is a base.
So none of the bullet points are applicable for the direct initialization of r by const Y and so X&& 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 allowing X&& 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:
The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference using direct-non-list-initialization. For this direct-initialization, user-defined conversions are not considered.
Activity
[-][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[/+]t3nsor commentedon Aug 14, 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).t3nsor commentedon Aug 14, 2024
For 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.
t3nsor commentedon Aug 14, 2024
How about in this example:
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.
jensmaurer commentedon Aug 14, 2024
This example very much looks like a narrowing situation to me. It should be ill-formed.
ranaanoop commentedon Aug 14, 2024
@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.ranaanoop commentedon Aug 14, 2024
@t3nsor Yes, the other option/alternative is to specify directly the
(E)form of direct initialization as mentioned in the original issue without theE.jensmaurer commentedon Aug 14, 2024
I'm referring to @t3nsor's latest example:
ranaanoop commentedon Aug 14, 2024
@jensmaurer Yes, that is why I included it in the new issue. I agree that it should also be ill-formed.
jensmaurer commentedon Aug 15, 2024
What does the current wording say about
#2? Is that well-formed?ranaanoop commentedon Aug 19, 2024
@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: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:
So we again move to dcl.init.list, this time to check the validity of
X resultObject = {z};:This means that
X's constructors(including the implicit copy ctor etc) will be enumerated for the argumentz. Now we move to over.match:Now, we go to over.match#copy:
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.jensmaurer commentedon Aug 20, 2024
The cross-reference in dcl.init.list points to [over.match.list], so I'd go there, not to over.match.copy.
ranaanoop commentedon Aug 20, 2024
@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.
jensmaurer commentedon Aug 20, 2024
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.
ranaanoop commentedon Aug 20, 2024
@jensmaurer I've already used over.match.list in my analysis. I've just not written it explicitly. In particular, [over.match.list] says:
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: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.ranaanoop commentedon Aug 21, 2024
@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.t3nsor commentedon Aug 23, 2024
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.t3nsor commentedon Aug 23, 2024
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.t3nsor commentedon Aug 23, 2024
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.ranaanoop commentedon Aug 23, 2024
@t3nsor Note that clang rejects
X x = {z};. Demo just like it rejectsX&& ref{z};. I think clang is having problem withX x = {z};which is why it also rejectsX&& ref{z};. Basically, as the validity ofX x = {z;}is part of the validity ofX&& ref{z};.t3nsor commentedon Aug 23, 2024
Oh yes, you're right. But all compilers accept
const X& r = z, so there is no rationale for Clang to rejectX x = {z};.t3nsor commentedon Aug 23, 2024
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.ranaanoop commentedon Aug 23, 2024
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:
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
t3nsor commentedon Sep 11, 2024
I'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:
(E)or whatever...t3nsor commentedon Jan 19, 2025
Jens, can we have a core issue for this please?
jensmaurer commentedon Jan 19, 2025
CWG2985
[-][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[/+]