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?
If any constructor is being called, it means a new object is being created in memory. So, the only difference between a copy constructor and a move constructor is whether the source object that is passed to the constructor will have its member fields copied or moved into the new object.
So, for every call of f we need to use the copy ctor 4 times, and since f gets called twice you already have +8 copy ctor calls, +1 for the final assignment in y , totaling 9 .
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.
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-qualifiedstd::initializer_list<E>for some typeE, 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 ofNelements of typeE, whereNis 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
(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 typeconst 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With