14
struct A {
    A(int) {}
};

struct B {
    B(A) {}
};

int main() {
    B b({0});
}

The construction of b gives the following errors:

In function 'int main()':
24:9: error: call of overloaded 'B(<brace-enclosed initializer list>)' is ambiguous
24:9: note: candidates are:
11:2: note: B::B(A)
10:8: note: constexpr B::B(const B&)
10:8: note: constexpr B::B(B&&)

I was expecting B::B(A) to be called, why is it ambiguous in this case?

cpplearner
  • 13,776
  • 2
  • 47
  • 72
ashen
  • 807
  • 9
  • 24

3 Answers3

3

B b({0}) can result in a call to either of the following:

  1. B::B(A)

  2. Copy constructor of B: constructing a temporary Bobject from {0} and then copying it over to b.

Hence the ambiguity.

It can be resolved if you call B b{0}, which directly uses the defined constructor with no copy constructor involvement.

EDIT:

Regarding how point 2 is valid:

B has a constructor which accepts A. Now, A can be constructed by an int. Also, int can be constructed via the initialization list. That's why this is a valid case. Had A's constructor been explicit, automatic casting from {0} to int would have failed, resulting in no ambiguity.

CinCout
  • 9,486
  • 12
  • 49
  • 67
  • 1
    This is what the compiler says, and it does not explain why it happens – Piotr Skotnicki Apr 28 '17 at 06:39
  • @PiotrSkotnicki Not sure what you mean by *"why it happens"*. Ambiguity occurs when the overload resolution finds more than one possibility, which is clearly the case here. – CinCout Apr 28 '17 at 07:44
  • 1
    Oh, so *ambiguity* results from an *ambiguous* call? I would never guess – Piotr Skotnicki Apr 28 '17 at 07:48
  • *"Ambiguity occurs when the overload resolution finds more than one possibility"*, the compiler already said which call is ambiguous, and listed candidates, the question is why the call is ambiguous under the current rules and whether it should be ambiguous – Piotr Skotnicki Apr 28 '17 at 08:01
  • @CinCout in most cases, when overload resolution finds more than one possibility, there is a single preferred possibility that wins the ranking process – M.M Apr 28 '17 at 08:09
  • 1
    I don't follow option 2. Why does `B` get constructed from an initializer list with a 0? I could easily be missing something but at the very least this is not obvious and deserves explanation. `B` does not have any constructors taking an integer, or an initialization list, and it doesn't even have a default constructor! – Nir Friedman Apr 28 '17 at 08:59
  • Added explanation. – CinCout Apr 28 '17 at 09:42
  • 2
    "*Had A's constructor been explicit, automatic casting from {0} to int would have failed, resulting in no ambiguity.*", did you try it ? – Piotr Skotnicki Apr 28 '17 at 11:21
  • @PiotrSkotnicki: It is no longer ambiguous, there are no valid candidates ;-) – Jarod42 Apr 28 '17 at 12:35
3

The code compiles fine with GCC8.

This shouldn't be ambiguous calling. For the copy/move constructor of B being invoked, then for B b({0}); the following steps are required:

  1. construct A from 0 by A::A(int)
  2. construct B from A constructed in step1 by B::B(A)
  3. construct b from B constructed in step2 by copy/move constructor of B.

That means two user-defined conversions (step#1 and #2) are required, but this is not allowed in one implicit convertion sequence.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • @ashen What compiler (and version) are you using? – songyuanyao Apr 28 '17 at 06:42
  • 2
    That is the correct answer. It fails on icc and vc++ v141 (2017) compilers. – unexpectedvalue Apr 28 '17 at 06:52
  • 1
    What happened to GCC 7? Did it get eaten by the same thing that ate Microsoft's Windows 9? – Cody Gray - on strike Apr 28 '17 at 09:50
  • @CodyGray branched off from trunk (8.0) – Piotr Skotnicki Apr 28 '17 at 11:56
  • @songyuanyao gcc 4.9.2. I've tried your link, it only compiles with gcc 8 under c++1z. If you try earlier versions, it still does not compile... – ashen Apr 28 '17 at 13:37
  • @ashen I noticed that too. I tried to find out if something changes from C++17 but failed. The code should work fine from C++11, as far as I can see. – songyuanyao Apr 28 '17 at 14:59
  • [\[over.match.funcs\]/p6](http://eel.is/c++draft/over.match.funcs#6) implies that a total of two user-defined conversions are allowed in a list-initialization sequence – Piotr Skotnicki May 02 '17 at 11:38
  • @PiotrSkotnicki I think there should be some explicit statements about that. But I can't find any in [dcl.init.list](http://eel.is/c++draft/dcl.init.list).. – songyuanyao May 02 '17 at 14:19
  • @songyuanyao unless it's an initialization list within another initialization list, a user-defined conversion is allowed for the elements; check the examples from [\[over.ics.list\]/p6](http://eel.is/c++draft/over.ics.list#6), `h({"foo"});` in particular – Piotr Skotnicki May 02 '17 at 15:05
3

Given a class, A with a user-defined constructor:

struct A
{
    A(int) {}
};

and another one, B, accepting A as a constructor parameter:

struct B
{
    B(A) {}
};

then in order to perform the initialization as below:

B b({0});

the compiler has to consider the following candidates:

B(A);         // #1
B(const B&);  // #2
B(B&&);       // #3

trying to find an implicit conversion sequence from {0} to each of the parameters.

Note that B b({0}) does not list-initialize b -- the (copy-)list-initialization applies to a constructor parameter itself.

Since the argument is an initializer list, the implicit conversion sequence needed to match the argument to a parameter is defined in terms of list-initialization sequence [over.ics.list]/p1:

When an argument is an initializer list ([dcl.init.list]), it is not an expression and special rules apply for converting it to a parameter type.

It reads:

[...], if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion. If multiple constructors are viable but none is better than the others, the implicit conversion sequence is the ambiguous conversion sequence. User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter types except as noted in 13.3.3.1.

For #1 to be viable, the following call must be valid:

A a = {0};

which is correct due to [over.match.list]/p1:

— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

i.e., class A has a constructor that accepts an int argument.

For #2 to be a valid candidate, the following call must be valid:

const B& b = {0};

which according to [over.ics.ref]/p2:

When a parameter of reference type is not bound directly to an argument expression, the conversion sequence is the one required to convert the argument expression to the referenced type according to [over.best.ics]. Conceptually, this conversion sequence corresponds to copy-initializing a temporary of the referenced type with the argument expression. Any difference in top-level cv-qualification is subsumed by the initialization itself and does not constitute a conversion.

translates to:

B b = {0};

Once again, following [over.ics.list]/p6:

User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter types [...]

the compiler is allowed to use the user-defined conversion:

A(int);

to convert the argument 0 to B's constructor parameter A.

For candidate #3, the same reasoning applies as in #2. Eventually, the compiler cannot choose between the aforementioned implicit conversion sequences {citation needed}, and reports ambiguity.

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160