C++ Copy Constructor Gets Called Instead of Initializer_List<>

C++ Copy constructor gets called instead of initializer_list

As pointed out by Nicol Bolas, the original version of this answer was incorrect: cppreference at the time of writing incorrectly documented the order in which constructors were considered in list-initialization. Below is an answer using the rules as they exist in the n4140 draft of the standard, which is very close to the official C++14 standard.

The text of the original answer is still included, for the record.


Updated Answer

Per NathanOliver's comment, gcc and clang produce different outputs in this situation:

g++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
initializer list

clang++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor

gcc is correct.

n4140 [dcl.init.list]/1

List-initialization is initialization of an object or reference from a braced-init-list.

You're using list-initialization there, and since c is an object, the rules for its list-initialization are defined in [dcl.init.list]/3:

[dcl.init.list]/3:

List-initialization of an object or reference of type T is defined as follows:

  1. If T is an aggregate...
  2. Otherwise, if the initializer list has no elements...
  3. Otherwise, if T is a specialization of std::initializer_list<E>...

going through the list so far:

  1. Foo is not an aggregate.
  2. It has one element.
  3. Foo is not a specialization of std::initializer_list<E>.

Then we hit [dcl.init.list]/3.4:

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). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

Now we're getting somewhere. 13.3.1.7 is also known as [over.match.list]:

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

  1. 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.
  2. 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.

So the copy constructor will only be considered after the initializer list constructors, in the second phase of overload resolution. The initializer list constructor should be used here.

It's worth noting that [over.match.list] then continues with:

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.

and that after [dcl.init.list]/3.5 deals with single-element list initialization:

Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element; if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.

which explains where cppreference got their special case for single-element list initialization, though they placed it higher in the order than it should be.


Original Answer

You're encountering an interesting aspect of list initialization, where if the list fulfills certain requirements it may be treated like a copy-initialization rather than a list-initialization.

from cppreference:

The effects of list initialization of an object of type T are:

If T is a class type and the initializer list has a single element of
the same or derived type (possibly cv-qualified), the object is
initialized from that element (by copy-initialization for
copy-list-initialization, or by direct-initialization for
direct-list-initialization). (since c++14)

Foo c{b} fulfills all these requirements.

Constructor taking std::initializer_list is preferred over other constructors

The compiler is pretty much never "free to choose" for stuff like this. If it were, we wouldn't be able to write pretty much any portable C++ code.

[over.match.list] does give priority to initializer_list constructors. Constructor function overloading under the rules of list initialization gets invoked at step 3.6. Steps 3.1-3.5 do not apply, as your type doesn't qualify for any of those cases. Step 3.1 is particularly interesting, as it is specifically meant to invoke copy constructors instead of doing other things, but it also only applies to aggregates. Which your type is not.

Since your type is implicitly convertible to int, and your type takes an initializer_list<int>, there is a valid way to build an initializer_list that matches a constructor for the type in question. Therefore, this is the constructor [over.match.list] will select.

So in this case, VC++ is wrong. As is Clang, apparently.

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.

Unable to use copy initialization (i.e =) to construct class with initializer list

When initializing an object with = the compiler implicitly creates a temporary object of your type and then copies it to your variable (although the temporary and copy may, and usually are, elided).

So your a object is theoretically equivalent to:

A a = A(std::set<int>({1, 2, 3}));

I'm not sure exactly why the behaviour differs but I think it's due to the fact that the compiler is allowed to only perform one user defined conversion implicitly. In this case it's probably considered two separate UDCs:

  1. initializer list to std::set
  2. std::set to A

C++ constructor taking an std::initializer_list of size one

Clang implemented DR 1467 (brace-initializing a T from a T behaves as if you didn't use braces) but has yet to implement DR 2137 (on second thought, do that only for aggregates).

Your code should return 2.

A possible workaround is Foo foo_test({ foo_zero });.



Related Topics



Leave a reply



Submit