Search code examples
c++c++17language-lawyerc++20standards-compliance

Why is this combination of move and value constructor ambigous for MSVC but not for Clang and GCC in C++17 and above


I have the following code

struct R {
    int i_ = 8;
    R() = default;
    R(R const& o) = default;
    R(R&& o) = default;
    R(int i) : i_(i) {}
    operator int() const { return i_; }
};
struct S {
    R i_;
    operator R() const { return i_; }
    operator int() const { return static_cast<int>(i_); }
};
int main() {
    S s;
    R r0(s);
    R r = static_cast<R>(s);
    float af[] = {1,2,3};
    float f1 = af[s];
    float f2 = af[r];
}

which compiles fine on Clang and GCC for C++17 and C++20 (not for C++ < 17) but complains about

'R::R': ambiguous call to overloaded function
note: could be 'R::R(R &&)'
note: or       'R::R(int)'

in MSVC for all available standards.

I tried to find a difference in the language conformances by comparing MSVC, Clang and GCC and also tried /permissive- for MSVC but couldn't find any explanation yet.

  • Who is right (any why) and
  • how can I make it compile for MSVC without giving up on the R&& and int i ctors in R or marking the cast operator int()s explicit?

Here's a rather crowded godbolt including a possible workaround.


Solution

  • This is CWG 2327, which doesn't currently have a resolution but gcc and clang seem to do "the right thing." The issue reads:

    Consider an example like:

     struct Cat {};
     struct Dog { operator Cat(); };
    
     Dog d;
     Cat c(d);
    

    This goes to 9.4 [dcl.init] bullet 17.6.2:

    Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (12.2.2.4 [over.match.ctor]), and the best one is chosen through overload resolution (12.2 [over.match]). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

    Overload resolution selects the move constructor of Cat. Initializing the Cat&& parameter of the constructor results in a temporary, per 9.4.4 [dcl.init.ref] bullet 5.2.1.2. This precludes the possitiblity of copy elision for this case.

    This seems to be an oversight in the wording change for guaranteed copy elision. We should presumably be simultaneously considering both constructors and conversion functions in this case, as we would for copy-initialization, but we'll need to make sure that doesn't introduce any novel problems or ambiguities.

    The case here is similar, just more complicated. With either:

    R r0(s);
    R r = static_cast<R>(s);
    

    We have a conversion function from s that gives a prvalue R, which is a better path to take than any of the other paths at our disposal -- whether doing through R's move constructor or R(int). But we just don't have a rule that says that that's what we're supposed to do.

    gcc and clang seem to implement the desired behavior on C++17 or later (where we have guaranteed copy elision) despite not having any wording for what the desired behavior is. msvc on the other hand seems to follow the rules as specified (which I think do specify this case as being ambiguous).