7

The following program:

#include <iostream>

struct A 
{ 
  A() { std::cout << "A()\n"; }
  A(int) { std::cout << "A(int)\n"; }
};

struct C {
  operator int() {
    std::cout << "operator int\n";
    return 42;
  }
  operator A() {
    std::cout << "operator A\n";
    return A();
  }
};

int main() {
    auto a = A{C{}};
};

compiles and, when run, prints:

operator A
A()

See Godbolt link.

This shows that the move constructor of A was selected to perform the initialization, with C::operator A() being invoked to convert C into the type that the move constructor expects.

Alternatively, the A::A(int) constructor could have been selected, with C::operator int() being invoked. But A::A(int) loses overload resolution, apparently.

Why does this happen? I am not able to see any rule in the standard that explains why the move constructor of A wins the overload resolution.

(This question is inspired by this answer; I could not understand why the code in that answer works.)

T.C.
  • 133,968
  • 17
  • 288
  • 421
Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 1
    I think because going via `int` requires 2 conversions but the standard only allows for 1 – Alan Birtles Feb 21 '21 at 22:37
  • @AlanBirtles I don't think that's why. Only one user-defined conversion is allowed to convert the argument type into the parameter type. In both cases, there is one user-defined conversion, namely the conversion operator. The call to the constructor of `A` that is ultimately selected by the overload resolution process doesn't count. That occurs after the overload resolution is done. – Brian Bi Feb 21 '21 at 22:39
  • It seems that the compiler will not pick `A(int)` due to copy elision, but I am not sure where does that rule come from. – Quimby Feb 21 '21 at 23:05
  • It does seem like mandatory-copy-elision is causing the move constructor to be preferred. Based on this [gcc bug report](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86521), and [this proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1155r3.html) it seems that's the intent but the standard is not very clear about it. Some (but not all) wording from that proposal seems to be merged into the working draft, so perhaps the current working draft resolves the ambiguity; I can't tell. – cigien Feb 21 '21 at 23:41
  • @cigien I don't understand the relevance of the proposal. It seems to be all about replacing copies with moves, not eliding moves entirely. – Brian Bi Feb 21 '21 at 23:45
  • I'm looking at [this section](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1155r3.html#wording) which refers to copy-initialization contexts. There's also wording about a second round of overload resolution if the first fails, which seems relevant here. I could be wrong about that. – cigien Feb 21 '21 at 23:48
  • @cigien "In the following copy-initialization contexts, a move operation might be used instead of a copy operation..." Nothing about eliding moves. – Brian Bi Feb 21 '21 at 23:50
  • Ok, probably not relevant then. The last line of comment#6 in the bug report does seem relevant, but I could be wrong about that as well. – cigien Feb 21 '21 at 23:51

1 Answers1

2

It appears that both GCC and Clang implemented a fix for CWG2327 by directly considering conversion functions to cv T as candidates when initializing an object of type cv2 T, in addition to constructors.

When C::operator A() is considered as a candidate itself, it is an exact match, and therefore prevails over the constructor call which requires a user-defined conversion.

That issue is still in drafting status, so you won't be finding this in the wording.

Notably both reject with -std=c++14.

T.C.
  • 133,968
  • 17
  • 288
  • 421