The latest version of clang (3.9) rejects this code on the second line of f
; the latest version of gcc (6.2) accepts it:
struct Y {
Y();
Y(const Y&);
Y(Y&&);
};
struct X {
operator const Y();
};
void f() {
X x;
Y y(x);
}
If any of these changes are made, clang will then accept the code:
Y
's move constructorconst
from the conversion operatorY y(x)
with Y y = x
Is the original example legal? Which compiler is wrong? After checking the sections about conversion functions and overload resolution in the standard I have not been able to find a clear answer.
When we're enumerating the constructors and check their viability - i.e. whether there is an implicit conversion sequence - for the move constructor, [dcl.init.ref]/5 falls through to the last bullet point (5.2.2), which was modified by core issues 1604 and 1571 (in that order).
The bottom line of these resolutions is that
If
T1
orT2
is a class type andT1
is not reference-related toT2
, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1T1
” by user-defined conversion (8.6, 13.3.1.4, 13.3.1.5); 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.
The first part just causes the conversion operator to be selected. So, according to the boldfaced part, we use const Y
to direct-initialize Y&&
. Again, we fall through until the last bullet point, which fails due to (5.2.2.3):
If
T1
is reference-related toT2
:
— cv1 shall be the same cv-qualification as, or greater cv-qualification than, cv2 ; and
However, this does not appertain to our original overload resolution anymore, which only sees that the conversion operator shall be used to direct-initialize the reference. In your example, overload resolution selects the move constructor because [over.ics.rank]/(3.2.5), and then the above paragraph makes the program ill-formed. This is a defect and has been filed as core issue 2077. A sensible solution would discard the move constructor during overload resolution.
All this makes sense wrt to your fixes: removing const
would prevent the fall-through since the types are now reference-compatible, and removing the move constructor leaves the copy constructor, which has a const reference (i.e. works as well). Finally, when we write Y y = x;
, instead of [dcl.init]/(17.6.2), (17.6.3) applies;
Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). [...]. The call is used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.
I.e. the initialization is effectively the same as Y y(x.operator const Y());
, which succeeds, because the move constructor is not viable (Y&& y = const Y
fails shallowly enough) and the copy constructor is selected.
I think this is a clang bug.
We start with [over.match.ctor]:
When objects of class type are direct-initialized (8.6), copy-initialized from an expression of the same or a derived class type (8.6), or default-initialized (8.6), overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized.
So we consider, for instance, the copy constructor. Is the copy constructor viable?
From [dcl.init.ref]:
— If the initializer expression [...] has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an rvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (see 13.3.1.6) then the reference is bound to the value of the initializer expression in the first case and to the result of the conversion in the second case.
Those candidate functions in [over.match.ref] are:
For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2”, respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion (4.5), are also candidate functions.
Which includes our operator const Y()
. Hence the copy constructor is viable. The move constructor is not (since you can't bind a non-const
rvalue reference to a const
rvalue), so we have exactly one viable candidate, which makes the program well-formed.
Er, as a followup, this is LLVM bug 16682, which makes it seem much more complicated than what I've initially laid out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With