C++ Copy constructor gets called instead of initializer_list<>
Asked Answered
M

2

18

Based on this code

struct Foo 
{
   Foo() 
   {
       cout << "default ctor" << endl;
   }

   Foo(std::initializer_list<Foo> ilist) 
   {
       cout << "initializer list" << endl;
   }

   Foo(const Foo& copy)
   {
       cout << "copy ctor" << endl;
   }
};

int main()
{

   Foo a;
   Foo b(a); 

   // This calls the copy constructor again! 
   //Shouldn't this call the initializer_list constructor?
   Foo c{b}; 



   _getch();
   return 0;
}

The output is:

default ctor

copy ctor

copy ctor

In the third case, I'm putting b into the brace-initialization which should call the initializer_list<> constructor.

Instead, the copy constructor takes the lead.

Will someone of you tell me how this works and why?

Mixologist answered 8/2, 2016 at 15:18 Comment(1)
Looks like something weird is going on g++ gives this while clang gives thisDyak
O
19

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.

Osculate answered 8/2, 2016 at 15:28 Comment(17)
Why then if I write Foo c{a,b} the output is first "copy ctor" and then "initializer-list" ?Mixologist
@Dunno std::initializer_list makes copies of its contents.Rodgerrodgers
@Dunno technically, it doesn't write "copy ctor initializer list" but "copy ctor copy ctor initializer list" as it copies both elements in the initializer list to create the initializer list. With more than one element in the list, the requirement "has a single element" is no longer fulfilled, so it chooses the initializer list constructor.Osculate
So in short, the initializer-list method calls as much times the copy-ctor as the number of Foo elements I write in the braces. And, finally, if the number of Foo elements is greater than 1, then and only then calls also the initializer list? Correct?Mixologist
@Dunno pretty much. Note that an empty initializer list will call the default copy constructor if it exists.Osculate
@Osculate Don't you mean « default constructor » instead of « default copy constructor »?Larcener
@Osculate I'm having troubles with your quotation in the answer. Look at this screenshot i.imgur.com/0grAUqq.jpg. I'm passing one parameter in the initializer-list and still I see copy ctor together with initializer list. So, the initializer list is always called when using { }, and the copy ctors I see are just those who are needed by the initializer-list constructor. Correct?Mixologist
@Mixologist if you're using gcc, it's a bug and is noted on the gcc bug tracker.Osculate
@Mixologist incorrect. The rules for actually calling the initializer-list constructor are actually fairly complicated, though they do make a nice list. At any rate, clang displays the expected behavior of calling the copy constructor in this instance. To figure out what should happen here, go down the list: the first statement fits the situation: a single element of type Foo inside the brackets, using direct-list-initialization...Osculate
then the initialization effectively used is direct-initialization, which selects the most fitting of all constructors with the set of supplied arguments, which here is a single value of type Foo. Since you have defined a copy constructor taking a single argument of type Foo, that is the constructor it selects. Again, this is the standard-specified behavior, which gcc apparently doesn't quite follow.Osculate
@jaggedSpire: cpprefence is incorrect. See my answer for details, but suffice it to say that GCC is right here.Cordelier
@NicolBolas it looks like the single element case cppreference mentions is simply lower priority than they have it listed as, placed as it is in [dcl.init.list]3.5. Still, this answer is wrong. May I correct it to use language from the standard, though this will make it quite similar to yours?Osculate
I'd offer to delete, but apparently you can't do that on an accepted answer (TIL)Osculate
@jaggedSpire: "May I correct it to use language from the standard, though this will make it quite similar to yours?" That's fine; I just want the information out there.Cordelier
@NicolBolas edited. Does it meet with your approval?Osculate
@NicolBolas I believe back when this question was posted (february), cppreference was reflecting the state of the language after wg21.link/cwg1467 but before wg21.link/cwg2137 . It changed since then.Fedak
Thanks for updating the answer. I notice that MSVC++ and Clang are printing the same thing, whereas GCC prints it's own version. It's a 2 versus 1. So in this case GCC is the winner? I'm a clang user, very sad about this... @FedakMixologist
C
6

Let us examine what the C++14 specification says about list initialization here. [dcl.init.list]3 has a sequence of rules which are to be applied in order:

3.1 does not apply, since Foo is not an aggregate.

3.2 does not apply, since the list is not empty.

3.3 does not apply, since Foo is not a specialization of initializer_list.

3.4 does apply, since Foo is a class type. It says to consider constructors with overload resolution, in accord with [over.match.list]. And that rule says to check initializer_list constructors first. Since your type has an initilaizer_list constructor, the compiler must check to see if an initializer_list matching one of those constructors can be manufactured from the given values. It can, so that is what must be called.

In short, GCC is right and Clang is wrong.

It should be noted that the C++17 working draft changes nothing about this. It has a new section 3.1 that has special wording for single-value lists, but that only applies to aggregates. Foo is not an aggregate, so it does not apply.

Cordelier answered 11/12, 2016 at 16:31 Comment(10)
Looks like this question mentions gcc and clang only in the comment section.Latten
@EdgarRokyan: True, but I found this question thanks to another question that was marked as a duplicate of this one. That question (which was needlessly deleted by its owner) had examples from Clang and GCC showing the differences between them.Cordelier
Frankly speaking, I am the owner :) I deleted it supposing that it was too similar to this one.Latten
@EdgarRokyan: Yes, being too similar is why it was closed as a duplicate. But deleting it means that I can't reference it. I can't point to similar code and show how GCC gets it right and Clang gets it wrong.Cordelier
Why is the copy costructor called before the initializer_list costructor? @NicolBolasMixologist
@gedamial: Because Clang has a bug in it. The standard makes it clear that the initializer_list constructor should be called. If your compiler does not do that, then it is buggy.Cordelier
@NicolBolas no, I was talking about GCC. Foo c{b} produces a call to the copy constructor and then to the initializer_list constructorMixologist
@gedamial: Yes. It has to create an initializer_list, which is a reference to a temporary array containing a copy of b. Then, c can be initialized with that initializer_list.Cordelier
@NicolBolas so, in short, the initializer_list needs a copy constructor for constructing itself, right? Had I written Foo c{b,d,e,f,g} there would've been a copy of all of those objects, right?Mixologist
@gedamial: initializer_list references an array, so it doesn't copy anything. When the compiler sees a braced-init-list and uses it to create an initializer_list, it also creates a temporary array that actually holds copies of the elements from the braced-init-lists. That is when the copy constructor is being invoked. It would be no different from doing auto c = {b, d, e, f, g};Cordelier

© 2022 - 2024 — McMap. All rights reserved.