Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens technically in this C++ code?

Tags:

c++

c++11

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?

like image 658
kayahr Avatar asked Jul 15 '14 07:07

kayahr


3 Answers

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.

like image 164
T.C. Avatar answered Nov 12 '22 01:11

T.C.


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
like image 5
Mine Avatar answered Nov 12 '22 01:11

Mine


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&)
like image 3
Manu343726 Avatar answered Nov 12 '22 00:11

Manu343726