Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clang and GCC disagree on legality of direct initialization with conversion operator

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:

  • Remove Y's move constructor
  • Remove const from the conversion operator
  • Replace Y 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.

like image 435
0x5f3759df Avatar asked Oct 12 '16 19:10

0x5f3759df


2 Answers

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 or T2 is a class type and T1 is not reference-related to T2, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” 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 to T2:
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.

like image 76
Columbo Avatar answered Nov 14 '22 09:11

Columbo


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.

like image 40
Barry Avatar answered Nov 14 '22 10:11

Barry