Why are copy and move constructors called together? [duplicate]
Asked Answered
B

2

21

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?

Bungle answered 25/8, 2017 at 11:16 Comment(5)
You can't move from initializer_list elements. The additional move in your second example inhibits copy elision and so you get an additional move on top of the copy.Lulululuabourg
Good question; the answer is buried in here: en.cppreference.com/w/cpp/language/list_initializationCounteraccusation
Inside a constructor that takes std::initializer_list in std::vector, those elements are no longer rvalue.Belmonte
Additionally, if you were to call v.push_back() a few times, until the vector exceeded its capacity and had to reallocate, you would get a bunch of copies rather than moves. To prevent that you would need to mark your move constructor noexcept.Kirit
@ArneVogel: I'm torn. One one hand the "update" to that question is very similar to this one. On the other hand those answers are woefully incomplete.Bowles
B
14

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.

Bowles answered 25/8, 2017 at 11:39 Comment(1)
Seems like a vector(std::initializer_list&&) would be useful :(Constable
C
5

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.

Crosshatch answered 25/8, 2017 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.