C++11 Member Initialization List Ambiguity

C++11 member initialization list ambiguity

Another way is to use braces:

version(unsigned major, unsigned minor, unsigned patch) :
major{major}, minor{minor}, patch{patch} { }

Then the macros will not interfere because they are function-like macros and need parentheses to be invoked.

C++11 uniform initialization: ambiguity between initializer list and multiple-parameter constructors?

it seems like the compiler interprets Foo b {1, 2} as a list
initialization, and calls constructor 2. Is the () syntax the only way
to force the compiler to consider other kinds of constructors when an
initializer-list constructor is present?

Quotes from standard draft explains this well:

9.4.5.2 [dcl.init.list] (emphasis mine):

A constructor is an initializer-list constructor if its first
parameter is of type std​::​initializer_­list or reference to cv
std​::​initializer_­list for some type E, and either there are no
other parameters or else all other parameters have default arguments
([dcl.fct.default]).

[Note 2: Initializer-list constructors are
favored
over other constructors in list-initialization
([over.match.list]). Passing an initializer list as the argument to
the constructor template template C(T) of a class C does not
create an initializer-list constructor, because an initializer list
argument causes the corresponding parameter to be a non-deduced
context ([temp.deduct.call]). — end note]

and 12.4.2.8 [over.match.list]:

When objects of non-aggregate class type T are list-initialized such
that [dcl.init.list] specifies that overload resolution is performed
according to the rules in this subclause or when forming a
list-initialization sequence according to [over.ics.list], overload
resolution selects the constructor in two phases:


  • If the initializer list is not empty or T has no default constructor,
    overload resolution is first performed where the candidate functions
    are the initializer-list constructors
    ([dcl.init.list]) of the class T
    and the argument list consists of the initializer list as a single
    argument.


  • Otherwise, or 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.

Ambigous constructor call with list-initialization

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.

Why does the number of elements in a initializer list cause an ambiguous call error?

What is happening here is that in the two element initializer list both of the string literals can be implicitly converted to const char* since their type is const char[N]. Now std::vector has a constructor that takes two iterators which the pointers qualify for. Because of that the initializer_list constructor of the std::vector<std::string> is conflicting with the iterator range constructor of std::vector<int>.

If we change the code to instead be

doSomething({"hello"s, "stack"s});

Then the elements of the initializer list are now std::strings so there is no ambiguity.

Ambiguity when using initializer list as parameter

The error is most likely being caused by this constructor template.

template<typename T0, typename T1>
EIGEN_DEVICE_FUNC
EIGEN_STRONG_INLINE Matrix(const T0& x, const T1& y)
{ ... }

Both that constructor and vector's initializer_list constructor are equally good matches in the function call f({x, y});, leading to the ambiguity error.

Here's a made up example with similar constructors and function calls that leads to an ambiguity error as well.

Why does direct list initialization causes ambiguity for type reference cast if cast operators to the type and reference to the type are declared?

List initialization, when used to initialize a reference, will take the listed values and convert them into a prvalue (aka: a temporary), which will be used to direct initialize the reference.

So int &b{foo{}} is functionally equivalent to int &b(int(foo{})). Which is ambiguous; it could generate that int via operator int or operator int&.

But even if it wasn't ambiguous, you would still be getting a non-const lvalue reference to a prvalue. Which is illegal. So this code was never going to work.

Braced-init-lists (curly braces) initialize objects, not references to objects. If you already have an object and want to get a reference to it, don't use braced-init-lists.


But in this case why does compiler accept #5?

Because list initialization is a series of rules with priority. A rule that has a higher priority than the one I pointed out above is the case of a braced-init-list which contains a single value, who's type is identical to the type of what is being initialized. #5 and 6 just so happen to fit that bill, since d, e, and a are all int&s.

But if you just take my advice and not use braced-init-lists when you're not trying to create an object, you won't have to worry about corner-cases like that.

are there any plans in C++ standard to address inconsistency of initializer list constructors?

Has there been any discussion or plans by the C++ standard committee to fix this type of ambiguity / unpleasantness?

There have been many fixes to initialization since C++11. For instance, you initially couldn't copy construct aggregates using list-initialization (CWG 1467). This really minor fix broke some code in an undesirable way that lead to a new issue to refine that previous fix to undo it if there's an initializer_list constructor (CWG 2137). It's hard to touch anything in these clauses without lots of unexpected consequences and breaking code, even in small cases. I doubt there will be a push to make any kind of large change to initialization in the future. At this point, the amount of code breakage would be tremendous.

The best solution is just to be aware about the pitfalls of initialization and be careful about what you're doing. My rule of thumb is to only use {}s when I deliberately need the behavior that {} provides, and () otherwise.

Note that this isn't really any different from the more well-known pitfall of:

vector<int> a{10}; // vector of 1 element
vector<int> b(10); // vector of 10 elements

One example would be requiring initializer list constructors to be called like this: vector<int> u({3}), which is already currently legal.

You have the same problem you had before, for the same reasons:

vector<int> u({3});    // vector of one element: 3
vector<string> v({3}); // vector of three elements: "", "", and ""

Even if you were to require the former (which would be impossible), you couldn't make the latter ill-formed.

Ambiguous overload resolution with initializer_list

The code is ill-formed. §8.5.4/(3.6) applies:

Otherwise, if T is a class type, constructors are considered. The
applicable constructors are enumerated and the best one is chosen
through overload resolution (13.3, 13.3.1.7).

Now, §13.3.3.1.5 goes

When an argument is an initializer list (8.5.4), it is not an expression and special rules apply for converting
it to a parameter type. […]
if the parameter type is std::initializer_list<X> and all
the elements of the initializer list can be implicitly converted to X,
the implicit conversion sequence is the worst conversion necessary to
convert an element of the list to X
, or if the initializer list has no
elements, the identity conversion.

Converting 1.1, which is of type double (!), to int is a Floating-integral conversion with Conversion rank, while the conversion from 1.1 to float is a Floating point conversion - also having Conversion rank.

Sample Image

Thus both conversions are equally good, and since §13.3.3.2/(3.1) cannot distinguish them either, the call is ambiguous. Note that narrowing doesn't play a role until after overload resolution is done and hence cannot affect the candidate set or the selection process. More precisely, a candidate must meet the requirement set in 13.3.2/3:

Second, for F to be a viable function, there shall exist for each
argument an implicit conversion sequence (13.3.3.1) that converts
that argument to the corresponding parameter of F.

However, as shown in the second quote, the implicit conversion sequence that converts {1.1} to std::initializer_list<int> is the worst conversion from 1.1 to int, which is a Floating-integral conversion - and a valid (and existing!) one at that.


If instead you pass {1.1f} or alter the initializer_list<float> to <double>, the code is well-formed, as converting 1.1f to float is an identity conversion. The standard gives a corresponding example in (3.6):

[ Example:

struct S {
S(std::initializer_list<double>); // #1
S(std::initializer_list<int>); // #2

};
S s1 = { 1.0, 2.0, 3.0 }; // invoke #1

end example ]

Even more interestingly,

struct S {
S(std::initializer_list<double>); // #1
S(std::initializer_list<int>); // #2

};
S s1 = { 1.f }; // invoke #1

Is also valid - because the conversion from 1.f to double is a Floating point promotion, having Promotion rank, which is better than Conversion rank.



Related Topics



Leave a reply



Submit