Reputation: 22010
I have a class B
which contains a vector of class A
. I want to initialize this vector through the constructor. Class A
outputs some debug info so I can see when it is constructed, destructed, copied or moved.
#include <vector>
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A::A" << endl; }
~A() { cout << "A::~A" << endl; }
A(const A& t) { cout <<"A::A(A&)" << endl; }
A(A&& t) { cout << "A::A(A&&)" << endl; }
};
class B {
public:
vector<A> va;
B(const vector<A>& va) : va(va) {};
};
int main(void) {
B b({ A() });
return 0;
}
Now when I run this program (Compiled with GCC option -fno-elide-constructors
so the move constructor calls are not optimized away) I get the following output:
A::A
A::A(A&&)
A::A(A&&)
A::A(A&)
A::A(A&)
A::~A
A::~A
A::~A
A::~A
A::~A
So instead of just one instance of A
the compiler generates five instances of it. A
is moved two times and it is copied two times. I didn't expect that. The vector is passed by reference to the constructor and then copied into the class field. So I would have expected a single copy-operation or even just a move operation (because I hoped the vector I pass to the constructor is just a rvalue), not two copies and two moves. Can someone please explain what exactly happens in this code? Where and why does it create all these copies of A
?
Upvotes: 45
Views: 4460
Reputation: 137301
It might be helpful to go through the constructor calls in reverse order.
B b({ A() });
To construct a B
, the compiler must call B's constructor that takes a const vector<A>&
. That constructor in turn must make a copy of the vector, including all of its elements. That's the second copy ctor call you see.
To construct the temporary vector to be passed to B
's constructor, the compiler must invoke the initializer_list
constructor of std::vector
. That constructor, in turn, must make a copy of what's contained in the initializer_list
*. That's the first copy constructor call you see.
The standard specifies how initializer_list
objects are constructed in §8.5.4 [dcl.init.list]/p5:
An object of type
std::initializer_list<E>
is constructed from an initializer list as if the implementation allocated an array of N elements of typeconst E
**, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and thestd::initializer_list<E>
object is constructed to refer to that array.
Copy-initialization of an object from something of the same type uses overload resolution to select the constructor to use (§8.5 [dcl.init]/p17), so with an rvalue of the same type it will invoke the move constructor if one is available. Thus, to construct the initializer_list<A>
from the braced initializer list, the compiler will first construct an array of one const A
by moving from the temporary A
constructed by A()
, causing a move constructor call, and then construct the initializer_list
object to refer to that array.
I can't figure out where the other move in g++ comes from, though. initializer_list
s are usually nothing more than a pair of pointers, and the standard mandates that copying one doesn't copy the underlying elements. g++ seems to call the move constructor twice when creating an initializer_list
from a temporary. It even calls the move constructor when constructing an initializer_list
from a lvalue.
My best guess is that it's implementing the standard's non-normative example literally. The standard provides the following example:
struct X { X(std::initializer_list<double> v); }; X x{ 1,2,3 };
The initialization will be implemented in a way roughly equivalent to this:**
const double __a[3] = {double{1}, double{2}, double{3}}; X x(std::initializer_list<double>(__a, __a+3));
assuming that the implementation can construct an initializer_list object with a pair of pointers.
So if you take this example literally, the array underlying the initializer_list
in our case will be constructed as if by:
const A __a[1] = { A{A()} };
which does incur two move constructor calls because it constructs a temporary A
, copy-initializes a second temporary A
from the first one, then copy-initializes the array member from the second temporary. The normative text of the standard, however, makes clear that there should only be one copy-initialization, not two, so this seems like a bug.
Finally, the first A::A
comes directly from A()
.
There's not much to discuss about the destructor calls. All temporaries (regardless of number) created during the construction of b
will be destructed at the end of the statement in reverse order of construction, and the one A
stored in b
will be destructed when b
goes out of scope.
* The initializer_list
constructors of standard library containers are defined as being equivalent to invoking the constructor taking two iterators with list.begin()
and list.end()
. Those member functions return a const T*
, so it can't be moved from. In C++14, the backing array is made const
, so it's even clearer that you can't possibly move from it or otherwise change it.
** This answer originally quoted N3337 (the C++11 standard plus some minor editorial changes), which has the array having elements of type E
rather than const E
and the array in the example being of type double
. In C++14, the underlying array was made const
as a result of CWG 1418.
Upvotes: 42
Reputation: 14174
Consider this:
A
is instanced: A()
A(A&&)
A(A&&)
.
EDIT: As T.C. noticed, initializer_list elements are not moved/copied for initializer_list moving/copying. As his code example shows, seems like two rvalue ctor calls are used during initializer_list initialization.A(const A&)
EDIT: Again, is not the vector but the initializer listA(const A&)
Upvotes: 3
Reputation: 64203
A::A
The constructor is executed, when the temporary object is created.
first A::A(A&&)
The temporary object is moved into the initialization list (which is also rvalue).
second A::A(A&&)
The initialization list is moved into vector's constructor.
first A::A(A&)
The vector is copied because the B's constructor takes the lvalue, and a rvalue is passed.
second A::A(A&)
Again, the vector is copied when creating the B's member variable va
.
A::~A
A::~A
A::~A
A::~A
A::~A
Destructor is called for every rvalue and lvalue (whenever the constructor, copy or move constructors are called, destructor is executed when the objects gets destroyed).
Upvotes: 0
Reputation: 4241
Try split the code a little to better understand the behavior:
int main(void) {
cout<<"Begin"<<endl;
vector<A> va({A()});
cout<<"After va;"<<endl;
B b(va);
cout<<"After b;"<<endl;
return 0;
}
The output is similar (note the -fno-elide-constructors
is used)
Begin
A::A <-- temp A()
A::A(A&&) <-- moved to initializer_list
A::A(A&&) <-- no idea, but as @Manu343726, it's moved to vector's ctor
A::A(A&) <-- copied to vector's element
A::~A
A::~A
A::~A
After va;
A::A(A&) <-- copied to B's va
After b;
A::~A
A::~A
Upvotes: 5