Overload resolution gets different result between

2019-04-24 16:57发布

问题:

struct A { A(int);};
struct B { explicit B(A); B(const B&);};
B b({0}); 

gcc 5.1.0 gives the error

/dev/fd/63:3:8: error: call of overloaded 'B(<brace-enclosed initializer list>)'
 is ambiguous
/dev/fd/63:3:8: note: candidates are:
/dev/fd/63:2:27: note: B::B(const B&)
/dev/fd/63:2:21: note: B::B(A)

while clang 3.6.0 succeeds.

Which one is right? Why?

For gcc 5.1.0: http://melpon.org/wandbox/permlink/pVe9eyXgu26NEX6X

For clang 3.6.0: http://melpon.org/wandbox/permlink/WOi1md2dc519SPW0

This may be similar to Direct list initialization compiles successfully, but normal direct initialization fails, why? which gcc and clang get same result.

But this is a different question. B(A) is explicit here. gcc and clang get different results.

回答1:

The difference can be reduced to

struct A { explicit A(int); };
struct B { B(int); };
void f(A);
void f(B);

int main() {
    f({ 1 });
}

On GCC this fails, in accordance to the Standard (which says that for list initialization, explicit constructors are considered - so they can yield an ambiguity - but they just are not allowed to be selected). Clang accepts it and calls the second function.

In your case, what @Columbo says in his answer to Direct list initialization compiles successfully, but normal direct initialization fails, why? applies. With the difference that in your case, B(const B&); is not anymore acceptable to Clang because the {0} -> B conversion would be faced with two possibilities: the explicit constructor or using the copy constructor recursively a second time. The first option, as explained above, will not be considered by clang and this time the explanation by @Columbo applies and the copy constructor cannot be used a second time because that would need a user defined conversion as we have a single element (here, 0). So in the summary, only the first constructor succeeds and is taken.


Since I understand the issue is about weird overload resolution rules and some might not be able to follow, here's a more intuitive explanation. The rules that are active are, in order

  • b({0}) means goto http://eel.is/c++draft/dcl.init#17 and from there to http://eel.is/c++draft/over.match.ctor which is our first OR context . The two constructors enumerated are B(A); and B(const B&) with argument {0}.

    • For B(A) it works with a single user defined conversion.

    • For B(const B&), we need to initialize a const B& which brings us to http://eel.is/c++draft/over.ics.list#8 then to http://eel.is/c++draft/over.ics.ref#2 (by help of http://eel.is/c++draft/dcl.init#dcl.init.list-3 "Otherwise, if T is a reference type, a prvalue temporary of the type referenced by T is copy-list-initialized ...") then to http://eel.is/c++draft/over.best.ics#over.ics.list-6. The resulting OR context has candidates B(A); and B(const B&), with the argument 0. This is our second OR context and is copy-list-initialization by 13.3.1.7 (as required by over.ics.ref#2 and dcl.init.list-3).

      • For B(A), the constructor is explicit and therefore ignored by Clang (in contradiction with the spec) but accepted by GCC (hence the ambiguity).

      • For B(const B&), this is the scenario handled by @Columbo and therefore the user-defined conversion which would be needed is forbidden. Newer drafts don't have this rule anymore (but probably it will be added back). But because the 0 to const B& would be a normal user-defined conversion (not a list initialization), it would ignore the explicit constructor needed for the conversion anyway (for this potential second use of the copy constructor), and therefore the user-defined conversion wouldn't be possible anyway and the rule is of much less significance than I thought when I wrote the above shorter summary.

Therefore for GCC it can use the explicit constructor directly, and in addition by a single use of the copy constructor. For clang, it only considers using the explicit constructor directly, and it won't use it indirectly by a copy-list-initialization using the copy constructor like GCC does. Both won't consider using the copy constructor a second time, and it's irrelevant here.



回答2:

The correct list initialization semantics is

B b{0};

which compiles fine. If you write B b({0});, the gcc can't decide if call B(A) directly or create B ({0}) and then copy it with B(const B&) in the second phase. There is no priority ordering between these two options.

It's language problem, not compiler's problem. See this gcc bug report.