Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11 constructor overload resolution and initialiser_lists: clang++ and g++ disagree

I have a small piece of C++11 code which g++ (4.7 or 4.8) refuses to compile claiming that the call to constructor for B2 b2a(x, {P(y)}) is ambiguous. Clang++ is happy with that code, but refuses to compile B2 b2b(x, {{P(y)}}) which g++ is perfectly happy to compile!

Both compilers are perfectly happy with the B1 constructor with either {...} or {{...}} as an argument. Can any C++ language lawyer explain which compiler is correct (if either) and what is going on? Code below:

#include <initializer_list>

using namespace std;

class Y {};
class X;

template<class T> class P {
public:
    P(T);
};

template<class T> class A {
public:
    A(initializer_list<T>);
};

class B1 {
public:
    B1(const X&, const Y &);
    B1(const X&, const A<Y> &);
};

class B2 {
public:
    B2(const X &, const P<Y> &);
    B2(const X &, const A<P<Y>> &);
};

int f(const X &x, const Y y) {
    B1 b1a(x, {y});
    B1 b1b(x, {{y}});
    B2 b2a(x, {P<Y>(y)});
    B2 b2b(x, {{P<Y>(y)}});
    return 0;
}

and the compiler errors, clang:

$ clang++ -stdlib=libc++ -std=c++11 test-initialiser-list-4.cc -o test.o -c 
test-initialiser-list-4.cc:32:6: error: call to constructor of 'B2' is ambiguous
  B2 b2(x, {{P<Y>(y)}});
     ^  ~~~~~~~~~~~~~~
test-initialiser-list-4.cc:26:5: note: candidate constructor
    B2(const X &, const P<Y> &);
    ^
test-initialiser-list-4.cc:27:5: note: candidate constructor
    B2(const X &, const A<P<Y>> &);
    ^

g++:

test-initialiser-list-4.cc: In function 'int f(const X&, Y)':
test-initialiser-list-4.cc:32:21: error: call of overloaded 'B2(const X&, <brace-enclosed initializer list>)' is ambiguous
   B2 b2(x, {P<Y>(y)});
                     ^
test-initialiser-list-4.cc:32:21: note: candidates are:
test-initialiser-list-4.cc:27:5: note: B2::B2(const X&, const A<P<Y> >&)
     B2(const X &, const A<P<Y>> &);
     ^
test-initialiser-list-4.cc:26:5: note: B2::B2(const X&, const P<Y>&)
     B2(const X &, const P<Y> &);
     ^

This smells like an interaction between uniform initialisation, initialiser list syntax and function overloading with templated arguments (which I know g++ is fairly stringent about), but I'm not enough of a standards lawyer to be able to unpack what should be the correct behaviour here!

like image 609
Phil Armstrong Avatar asked Jul 17 '13 16:07

Phil Armstrong


1 Answers

First code, then what I think should happen. (In what follows, I will ignore the first parameter, since we are interested only into the second parameter. The first one is always an exact match in your example). Please note that the rules are currently in flux in the spec, so I wouldn't say that one or the other compiler has a bug.

B1 b1a(x, {y});

This code cannot call the const Y& constructor in C++11, because Y is an aggregate and Y has no data member of type Y (of course) or something else initializable by it (this is something ugly, and is worked on to be fixed - the C++14 CD doesn't have wording for this yet, so I am not sure whether final C++14 will contain this fix).

The constructor with the const A<Y>& parameter can be called - {y} will be taken as the argument to the constructor of A<Y>, and will initialize that constructor's std::initializer_list<Y>.

Hence - second constructor called successfully.

B1 b1b(x, {{y}});

Here, the basically same argument counts counts for the constructor with the const Y& parameter.

For the constructor with parameter type const A<Y>&, it is a bit more complicated. The rule for conversion cost in overload resolution computing the cost of initializing an std::initializer_list<T> requires every element of the braced list to be convertible to T. However we before said that {y} cannot be converted to Y (as it is an aggregate). It is now important to know whether std::initializer_list<T> is an aggregate or not. Frankly, I have no idea whether or not it must be considered to be an aggregate according to the Standard library clauses.

If we take it to be a non-aggregate, then we would be considering the copy constructor of std::initializer_list<Y>, which however again would trigger the exact same sequence of tests (leading to "infinite recursion" in overload resolution checking). Since this is rather weird and non-implementable, I don't think any implementation takes this path.

If we take std::initializer_list to be an aggregate, we will be saying "nope, no conversion found" (see the above aggregates-issue). In that case since we cannot call the initializer constructor with the single initializer list as a whole, {{y}} will be split up into multiple arguments, and the constructor(s) of A<Y> will be taking each of those separately. Hence, in this case, we would end up with {y} initializing a std::initializer_list<Y> as the single parameter - which is perfectly fine and work like a charm.

So under the assumption that std::initializer_list<T> is an aggregate, this is fine and call the second constructor successfully.

B2 b2a(x, {P<Y>(y)});

In this case and the next case, we don't have the aggregate issue like above with Y anymore, since P<Y> has a user-provided constructor.

For the P<Y> parameter constructor, that parameter will be initialized by {P<Y> object}. As P<Y> has no initializer lists, the list will be split up into individual arguments and call the move-constructor of P<Y> with an rvalue object of P<Y>.

For the A<P<Y>> parameter constructor, it is the same as the above case with A<Y> initialized by {y}: Since std::initializer_list<P<Y>> can be initialized by {P<Y> object}, the argument list is not split, and hence the braces are used to initializer that constructor'S std::initializer_list<T>.

Now, both constructors work fine. They are acting like overloaded functions here, and their second parameter in both cases requires a user defined conversion. User defined conversion sequences can only be compared if in both cases the same conversion function or constructor is used - not the case here. Hence, this is ambiguous in C++11 (and in the C++14 CD).

Note that here we have a subtle point to explore

struct X { operator int(); X(){/*nonaggregate*/} };

void f(X);
void f(int);

int main() {
  X x;
  f({x}); // ambiguity!
  f(x); // OK, calls first f
}

This counter intuitive result will probably be fixed in the same run with fixing the aggregate-initialization weirdness mentioned above (both will call the first f). This is implemented by saying that {x}->X becomes an identity conversion (as is X->x). Currently, it is a user-defined conversion.

So, ambiguity here.

B2 b2b(x, {{P<Y>(y)}});

For the constructor with parameter const P<Y>&, we again split the arguments and get {P<Y> object} argument passed to the constructor(s) of P<Y>. Remember that P<Y> has a copy constructor. But the complication here is that we are not allowed to use it (see 13.3.3.1p4), because it would require a user defined conversion. The only constructor left is the one taking Y, but an Y cannot be initialized by {P<Y> object}.

For the constructor with parameter A<P<Y>>, the {{P<Y> object}} can initialize a std::initializer_list<P<Y>>, because {P<Y> object} is convertible to P<Y> (other than with Y above - dang, aggregates).

So, second constructor called successfully.


Summary for all 4

  • second constructor called successfully
  • under the assumption that std::initializer_list<T> is an aggregate, this is fine and call the second constructor successfully
  • ambiguity here
  • second constructor called successfully
like image 64
Johannes Schaub - litb Avatar answered Sep 28 '22 06:09

Johannes Schaub - litb