Calling an Explicit Constructor with a Braced-Init List: Ambiguous or Not

Calling an explicit constructor with a braced-init list: ambiguous or not?

As far as I can tell, this is a clang bug.

Copy-list-initialization has a rather unintuitive behaviour: It considers explicit constructors as viable until overload resolution is completely finished, but can then reject the overload result if an explicit constructor is chosen. The wording in a post-N4567 draft, [over.match.list]p1

In copy-list-initialization, if an explicit constructor is chosen, the
initialization is ill-formed. [ Note: This differs from other
situations (13.3.1.3, 13.3.1.4), where only converting constructors
are considered for copy-initialization. This restriction only applies
if this initialization is part of the final result of overload
resolution. — end note ]


clang HEAD accepts the following program:

#include <iostream>
using namespace std;

struct String1 {
explicit String1(const char*) { cout << "String1\n"; }
};
struct String2 {
String2(const char*) { cout << "String2\n"; }
};

void f1(String1) { cout << "f1(String1)\n"; }
void f2(String2) { cout << "f2(String2)\n"; }
void f(String1) { cout << "f(String1)\n"; }
void f(String2) { cout << "f(String2)\n"; }

int main()
{
//f1( {"asdf"} );
f2( {"asdf"} );
f( {"asdf"} );
}

Which is, except for commenting out the call to f1, straight from Bjarne Stroustrup's N2532 - Uniform initialization, Chapter 4. Thanks to Johannes Schaub for showing me this paper on std-discussion.

The same chapter contains the following explanation:

The real advantage of explicit is that it renders f1("asdf") an
error. A problem is that overload resolution “prefers” non-explicit
constructors, so that f("asdf") calls f(String2). I consider the
resolution of f("asdf") less than ideal because the writer of
String2 probably didn’t mean to resolve ambiguities in favor of
String2 (at least not in every case where explicit and non-explicit
constructors occur like this) and the writer of String1 certainly
didn’t. The rule favors “sloppy programmers” who don’t use explicit.


For all I know, N2640 - Initializer Lists — Alternative Mechanism and Rationale is the last paper that includes rationale for this kind of overload resolution; it successor N2672 was voted into the C++11 draft.

From its chapter "The Meaning Of Explicit":

A first approach to make the example ill-formed is to require that all
constructors (explicit and non-explicit) are considered for implicit
conversions, but if an explicit constructor ends up being selected,
that program is ill-formed. This rule may introduce its own surprises;
for example:

struct Matrix {
explicit Matrix(int n, int n);
};
Matrix transpose(Matrix);

struct Pixel {
Pixel(int row, int col);
};
Pixel transpose(Pixel);

Pixel p = transpose({x, y}); // Error.

A second approach is to ignore the explicit constructors when looking
for the viability of an implicit conversion, but to include them when
actually selecting the converting constructor: If an explicit
constructor ends up being selected, the program is ill-formed. This
alternative approach allows the last (Pixel-vs-Matrix) example to work
as expected (transpose(Pixel) is selected), while making the
original example ("X x4 = { 10 };") ill-formed.

While this paper proposes to use the second approach, its wording seems to be flawed - in my interpretation of the wording, it doesn't produce the behaviour outlined in the rationale part of the paper. The wording is revised in N2672 to use the first approach, but I couldn't find any discussion about why this was changed.


There is of course slightly more wording involved in initializing a variable as in the OP, but considering the difference in behaviour between clang and gcc is the same for the first sample program in my answer, I think this covers the main points.

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.

Ambiguous overloads, implicit conversion and explicit constructors

You're right that it seems like a bug. The commented out line below fails to compile for the exact reason that there should be no ambiguity.

Got a workaround for you using std::initializer_list:

#include <fmt/core.h>
#include <vector>

class A {
int a;
int b;

public:
explicit A() = default;
A(int _a, int _b) : a(_a), b(_b) {fmt::print("Aab\n");}

void f(const A& a) { fmt::print("A\n"); }
void f(std::vector<int> a) { fmt::print("vector\n"); }
void f(std::initializer_list<int> l) {
return f(std::vector<int>(l));
}
};
void g(const A& a) { fmt::print("A\n"); }
void g(std::vector<int> a) { fmt::print("vector\n"); }
void g(std::initializer_list<int> a) {return g(std::vector<int>(a)); }

int main() {
A a(1,2);
A a2 = A();
//A a3 = {};
a.f({});
g({});
return 0;
}

What could go wrong if copy-list-initialization allowed explicit constructors?

Conceptually copy-list-initialization is the conversion of a compound value to a destination type. The paper that proposed wording and explained rationale already considered the term "copy" in "copy list initialization" unfortunate, since it doesn't really convey the actual rationale behind it. But it is kept for compatibility with existing wording. A {10, 20} pair/tuple value should not be able to copy initialize a String(int size, int reserve), because a string is not a pair.

Explicit constructors are considered but forbidden to be used. This makes sense in cases as follows

struct String {
explicit String(int size);
String(char const *value);
};

String s = { 0 };

0 does not convey the value of a string. So this results in an error because both constructors are considered, but an explicit constructor is selected, instead of the 0 being treated as a null pointer constant.

Unfortunately this also happens in overload resolution across functions

void print(String s);
void print(std::vector<int> numbers);

int main() { print({10}); }

This is ill-formed too because of an ambiguity. Some people (including me) before C++11 was released thought that this is unfortunate, but didn't came up with a paper proposing a change regarding this (as far as I am aware).

Possible regression in G++ 6.1.0

This is a surprising and somewhat unfortunate aspect of the Standard (I would go so far as to call it a defect); it is the result of a collision between the overload resolution rules for copy-list-initialization ([over.match.list] as confirmed in CWG 1228), and the element-forwarding constructor of pair (as per n4387).

gcc (>= 6.1.0) is correct to reject your program; clang is incorrect to accept it. Earlier versions of gcc accept your program because they had not yet implemented n4387; clang accepts your program because it excludes explicit constructors from consideration for overload resolution for copy-list-initialization, which violates [over.match.list] according to the Standard (Calling an explicit constructor with a braced-init list: ambiguous or not?)


If we peel away the extraneous aspects of your program it comes down to a simple question of overload resolution:

struct A { explicit A(int, int); };
struct B { B(int, int); };

void f(A);
void f(B);

int main() { f({0, 0}); }

Here A is standing in for pair<std::string const, AV> and B is standing in for pair<string const, string>. The constructor of A is explicit because pace n4387 it involves the explicit constructor of AV; but per CWG 1228 the rules for copy-list-initialization:

[...] include all constructors but state that the program is ill-formed if an explicit constructor is selected by overload resolution. [...]

[over.match.list]:

[...] In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed. [ Note: This differs from other situations ([over.match.ctor], [over.match.copy]), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution. — end note ]

Thus your program is correctly considered (under the Standard as it currently is) to be ambiguous.

Further reading: What could go wrong if copy-list-initialization allowed explicit constructors?

Explicit constructors and nested initializer lists

Instead of explaining the behavior of compilers, I'll try to explain what the standard says.

Primary Example

To direct-initialize b1 from {{{"test"}}}, overload resolution applies to choose the best constructor of B. Because there is no implicit conversion from {{{"test"}}} to B& (list initializer is not a lvalue), the constructor B(B&) is not viable. We then focus on the constructor B(A), and check whether it is viable.

To determine the implicit conversion sequence from {{{"test"}}} to A (I will use the notation {{{"test"}}} -> A for simplicity), overload resolution applies to choose the best constructor of A, so we need to compare {{"test"}} -> const char* and {{"test"}} -> std::string (note the outermost layer of braces is elided) according to [over.match.list]/1:

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, overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T...

  • 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.


... In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

Note all constructors are considered here regardless of the specifier explicit.

{{"test"}} -> const char* does not exist according to [over.ics.list]/10 and [over.ics.list]/11:

Otherwise, if the parameter type is not a class:

  • if the initializer list has one element that is not itself an initializer list...

  • if the initializer list has no elements...


In all cases other than those enumerated above, no conversion is possible.

To determine {{"test"}} -> std::string, the same process is taken, and overload resolution chooses the constructor of std::string that takes a parameter of type const char*.

As a result, {{{"test"}}} -> A is done by choosing the constructor A(std::string).


Variations

What if explicit is removed?

The process does not change. GCC will choose the constructor A(const char*) while Clang will choose the constructor A(std::string). I think it is a bug for GCC.

What if there are only two layers of braces in the initializer of b1?

Note {{"test"}} -> const char* does not exist but {"test"} -> const char* exists. So if there are only two layers of braces in the initializer of b1, the constructor A(const char*) is chosen because {"test"} -> const char* is better than {"test"} -> std::string. As a result, an explicit constructor is chosen in copy-list-initialization (initialization of the parameter A in the constructor B(A) from {"test"}), then the program is ill-formed.

What if the constructor B(const B&) is declared?

Note this also happens if the declaration of B(B&) is removed. This time we need to compare {{{"test"}}} -> A and {{{"test"}}} -> const B&, or {{{"test"}}} -> const B equivalently.

To determine {{{"test"}}} -> const B, the process described above is taken. We need to compare {{"test"}} -> A and {{"test"}} -> const B&. Note {{"test"}} -> const B& does not exist according to [over.best.ics]/4:

However, if the target is

— the first parameter of a constructor or

— the implicit object parameter of a user-defined conversion function

and the constructor or user-defined conversion function is a candidate by

— [over.match.ctor], when the argument is the temporary in the second step of a class copy-initialization,

— [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or

the second phase of [over.match.list] when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or
reference to cv X
,

user-defined conversion sequences are not considered.

To determine {{"test"}} -> A, the process described above is taken again. This is almost the same as the case we talked in the previous subsection. As a result, the constructor A(const char*) is chosen. Note the constructor is chosen here to determine {{{"test"}}} -> const B, and does not apply actually. This is permitted though the constructor is explicit.

As a result, {{{"test"}}} -> const B is done by choosing the constructor B(A), then the constructor A(const char*). Now both {{{"test"}}} -> A and {{{"test"}}} -> const B are user-defined conversion sequences and neither is better than the other, so the initialization of b1 is ambiguous.

What if the parentheses is replaced by braces?

According to [over.best.ics]/4, which is block-quoted in the previous subsection, the user defined conversion {{{"test"}}} -> const B& is not considered. So the result is the same as the primary example even if the constructor B(const B&) is declared.

Why is the std::initializer_list constructor preferred when using a braced initializer list?

§13.3.1.7 [over.match.list]/p1:

When objects of non-aggregate class type T are list-initialized
(8.5.4), overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of
    the initializer list as a single argument.
  • 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.

If the initializer list has no elements and T has a default
constructor, the first phase is omitted. In copy-list-initialization,
if an explicit constructor is chosen, the initialization is
ill-formed.

As long as there is a viable initializer-list constructor, it will trump all non-initializer-list constructors when list-initialization is used and the initializer list has at least one element.

Very strange overload failure

What goes wrong?

Bar A({1,2});

Can be interpreted as:

Bar A(Bar{Foo<std::size_t>(1), (bool)2 /*, true*/ });

or

Bar A(Foo<std::size_t>{1,2} /*, true, true*/);

so ambiguous call.

How can I solve it?

It depends of which result you expect, adding explicit might help for example.

Making explicit Foo(size_t n) would allow only:

Bar A(B{Foo<std::size_t>(1), (bool)2 /*, true*/ });


Related Topics



Leave a reply



Submit