Why are copy and move constructors called together?

1.7k views Asked by At

Consider the following code:

#include <iostream>
#include <vector>
using namespace std;

class A
{
public:
     A(int) { cout << "int" << endl; }
     A(A&&) { cout << "move" << endl; }
     A(const A&) { cout << "copy" << endl; }
};

int main()
{
    vector<A> v
    {
        A(10), A(20), A(30)
    };

    _getch();
    return 0;
}

The output is:

int
int
int
copy
copy
copy

A(10), A(20) and A(30) are temporaries, right?

So why is the copy constructor called? Shouldn't the move constructor be called instead?

Passing move(A(10)), move(A(20)), move(A(30)) instead, the output is:

int
move
int
move
int
move
copy
copy
copy

In this case either copy or move constructor is called.

What's happening?

2

There are 2 answers

1
AndyG On BEST ANSWER

std::vector can be constructed from a std::initializer_list, and you are calling that constructor. The rules for initializer_list construction state that this constructor is aggressively preferred:

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6). [ Note: Initializer-list constructors are favored over other constructors in list-initialization <...>]

Also, because of the sort of weird implementation of an initializer_list as an array allocated under the hood, elements of the corresponding array that the std::initializer_list<E> refers to are forced to be copy initialized (which can be elided):

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

(Both references above from N3337 [dcl.init.list])

However, in your first example the copies can/are elided despite the name ([dcl.init]/14) so you don't see an extra copy construction (they can also be moved) You can thank your compiler for that, because copy elision is not required in C++11 (although it is in C++17).

See [class.copy] for more details ("When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object...").

The final part is key:

[support.initlist] states that

An object of type initializer_list<E> provides access to an array of objects of type const E.

This means that the std::vector cannot take over the memory directly; it must be copied, this is where you ultimately see the copy constructions being called.

In the second example it is as Kerrek SB stated, you prevented the copy-elision I mentioned earlier and caused an additional overhead of a move.

0
eerorika On

A(10), A(20), A(30) are temporaries, right?

Correct.

So why the copy constructor is called? Shouldn't the move constructor be called instead?

Unfortunately, it is not possible to move from std::initializer_list, which is what this constructor of std::vector uses.

Passing move(A(10)), move(A(20)), move(A(30)) instead

In this case either copy or move constructor are called. What's happening?

Because the std::move conversion prevents copy-elision, and so the elements of the std::initializer_list are move constructed without elision. Then the constructor of vector copies from the list.