Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambigous constructor call with list-initialization

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?

like image 529
ashen Avatar asked Apr 28 '17 05:04

ashen


3 Answers

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.

like image 139
Piotr Skotnicki Avatar answered Oct 30 '22 13:10

Piotr Skotnicki


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.

like image 3
songyuanyao Avatar answered Oct 30 '22 15:10

songyuanyao


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.

like image 3
CinCout Avatar answered Oct 30 '22 13:10

CinCout