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 ofN
elements of typeE
, whereN
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 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