Ambigous constructor call with list-initialization
Asked Answered
F

3

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?

Frondescence answered 28/4, 2017 at 5:11 Comment(0)
C
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.

Consortium answered 1/5, 2017 at 14:20 Comment(2)
I'm not entirely sure, but probably it goes like thisConsortium
seems better explained to me.Frondescence
M
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.

Meeker answered 28/4, 2017 at 6:22 Comment(9)
This is what the compiler says, and it does not explain why it happensConsortium
@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.Meeker
Oh, so ambiguity results from an ambiguous call? I would never guessConsortium
"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 ambiguousConsortium
@Meeker in most cases, when overload resolution finds more than one possibility, there is a single preferred possibility that wins the ranking processSharilyn
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!Calen
Added explanation.Meeker
"Had A's constructor been explicit, automatic casting from {0} to int would have failed, resulting in no ambiguity.", did you try it ?Consortium
@PiotrSkotnicki: It is no longer ambiguous, there are no valid candidates ;-)Blades
C
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.

Cavell answered 28/4, 2017 at 6:35 Comment(9)
@Frondescence What compiler (and version) are you using?Cavell
That is the correct answer. It fails on icc and vc++ v141 (2017) compilers.Presto
What happened to GCC 7? Did it get eaten by the same thing that ate Microsoft's Windows 9?Stockbreeder
@CodyGray branched off from trunk (8.0)Consortium
@Cavell 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...Frondescence
@Frondescence 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.Cavell
[over.match.funcs]/p6 implies that a total of two user-defined conversions are allowed in a list-initialization sequenceConsortium
@PiotrSkotnicki I think there should be some explicit statements about that. But I can't find any in dcl.init.list..Cavell
@Cavell 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, h({"foo"}); in particularConsortium
C
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.

Consortium answered 1/5, 2017 at 14:20 Comment(2)
I'm not entirely sure, but probably it goes like thisConsortium
seems better explained to me.Frondescence

© 2022 - 2024 — McMap. All rights reserved.