What happens technically in this C++ code?
Asked Answered
C

4

45

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?

Coss answered 15/7, 2014 at 7:56 Comment(16)
Maybe there are no multiple instances, just five constructor calls? C++ 11 allows to delegate constructors.Henden
@Henden There are also five calls to the destructor. So there must be five different instances.Coss
When B is constructed, what's the capacity of va? Maybe more memory has been reserved for it than is used initially.Strontian
There's no delegating constructor anywhere in the definition of A. Interestingly, clang calls the move ctor only once.Calcicole
that is not uniform initialization, it looks like you are going via initializer_listDecrement
@Strontian The initial capacity is 0. When I pass an empty vector to the constructor then no instances of A are created.Coss
@Calcicole Then this is all / partially compiler dependent ...?Strontian
VS2012 (no initializer lists!): I have to modify the code: vector<A> aa; aa.push_back(A()); B b(aa);. Output: A::A A::A(A&&) A::~A A::A(A&)Strontian
@Strontian That's really all the output you get? So VS2012 generates a memory leak? There are two destructor calls missing.Coss
@Strontian The other two dtor calls in your code happen when main finishes. I'm guessing that you used cin.get() or a breakpoint or something similar to look at the output, so they haven't gotten a chance to run.Calcicole
@Calcicole Indeed. Starting the executable from a console shows that the dtors are called after the copy ctor (and after I pressed a key :-) ).Strontian
If you want to be able to move from the vector in B's constructor, you'll need to either provide an overload taking vector<A> && or take the vector by value and do : va(std::move(va)). If you take a const ref, it's always going to copy.Calcicole
No matter how I think about this, it should be 4 instances, not 5. BefuddledDecrement
@Calcicole Thanks for the hint. Now with enabled elide-constructor optimization it is only a single copy operation. Also works when I provide an overload taking const initializer_list<A>&. If you have an idea how to get rid of the last remaining copy-operation then please let me know. Would be nice to move the initializer list right into B.va without copying anything.Coss
Use a debugger to plant breakpoints where you use cout, then you can see where the calls come from.Fictional
@Coss Like I discussed in my answer, you can't avoid copying from an initializer_list, because there's no way to move from its elements. You'll have to manually populate the vector (with either the rvalue reference overload of push_back or simply emplace_back) and then std::move that vector to B's constructor.Calcicole
C
42

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 type const 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 the std::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_lists 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.

Calcicole answered 15/7, 2014 at 8:18 Comment(6)
What about the two copy ctor calls? One is from A::va by value lvalue initialization (i.e. copy), but the former?Ecclesiasticism
@Ecclesiasticism 3rd paragraph. It comes from constructing the temporary vector from the initializer_list.Calcicole
Thats exactly what struggles me. I have identified before that comes from the temporal vector initialization (See point 4 of my answer), but what I don't understand is why the compiler selected copy initialization instead of move. Is not the initializar list treated as an rvalue?Ecclesiasticism
@Ecclesiasticism There's no interface for modifying elements stored in an initializer_list, so you can't move from it. The only way to access the elements stored is via begin() and end(), and both return const T*.Calcicole
The non-normative example is disconcerting. I wonder if the intention of double{1}... was to just cast the int to a double; it would have been better if the sample read X x{ 1.0,2.0,3.0 };Inexorable
Its really anoying that something designed as an initializer (So it will be used as an rvalue almost always) doesn't support move semantics.... But thanks, that makes sense.Ecclesiasticism
B
5

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
Beebeebe answered 15/7, 2014 at 8:38 Comment(0)
E
3

Consider this:

  1. The temporary A is instanced: A()
  2. That instance is moved to the initializer list: A(A&&)
  3. The initializer list is moved to the vector ctor, so its elements are moved: 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.
  4. The vector element is initialized by value, instead of by move (Why?, I'm not sure): A(const A&) EDIT: Again, is not the vector but the initializer list
  5. Your ctor gets that temporal vector and copies it (Note your vector initializer), so the elements are copied: A(const A&)
Ecclesiasticism answered 15/7, 2014 at 8:24 Comment(1)
Copying/moving initializer_list doesn't copy or move the underlying elements, so that second move constructor call is weird.Calcicole
I
0

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

Infernal answered 15/7, 2014 at 8:47 Comment(1)
This answer is exactly as mine, but not figuring out whats really happening here: Note that, as T.C. said, an initializer_list doesn't move/copy its elements during copy/move. Also, is initializer list, not initialization list. Also, the vector is not copied twice, the vector is only copied when initializing A::va, the A ctor takes an lvalue reference, which also can reference an rvalue. Definitively -1Ecclesiasticism

© 2022 - 2024 — McMap. All rights reserved.