Given class X
below (special member functions other than the one explicitly defined are not relevant for this experiment):
struct X
{
X() { }
X(int) { }
X(X const&) { std::cout << "X(X const&)" << std::endl; }
X(X&&) { std::cout << "X(X&&)" << std::endl; }
};
The following program creates a vector of objects of type X
and resizes it so that its capacity is exceeded and reallocation is forced:
#include <iostream>
#include <vector>
int main()
{
std::vector<X> v(5);
v.resize(v.capacity() + 1);
}
Since class X
provides a move constructor, I would expect the previous content of the vector to be moved into the new storage after reallocation. Quite surprisingly, that does not seem to be the case, and the output I get is:
X(X const&)
X(X const&)
X(X const&)
X(X const&)
X(X const&)
Why?
The C++ function std::vector::resize() changes the size of vector. If n is smaller than current size then extra elements are destroyed. If n is greater than current container size then new elements are inserted at the end of vector. If val is specified then new elements are initialed with val.
When inserted using the push_back, the new element is copy-or-move-constructed. The insertion could be inefficient if the passed argument to the push_back is a temporary because the temporary is constructed, copied/moved, and destroyed.
Resizing a vector doesn't destroy the values stored in the vector (except for those beyond the new size when shrinking, of course), however growing a vector beyond its capacity will copy (or, in C++11, move) them to a new place, thus invalidating and iterators, pointers or references to those elements.
So you can emplace_back does use the desired constructor to create the element and call copy constructor when it need to grow the storage. You can call reserve with enough capacity upfront to avoid the need to call copy constructor.
Paragraph 23.3.6.3/14 of the C++11 Standard specifies (about the resize()
member function of the vector<>
class template):
Remarks: If an exception is thrown other than by the move constructor of a non-
CopyInsertable T
there are no effects.
In other words, this means that for X
(which is CopyInsertable
), resize()
offers the strong guarantee: it either succeeds or leaves the state of the vector unchanged.
In order to satisfy this guarantee, implementations usally adopt the copy-and-swap idiom: if the copy constructor of X
throws, we haven't altered the content of the original vector yet, so the promise is kept.
However, if the previous content of the vector were moved into the new storage rather than being copied and the move constructor threw, then we would have irreversibly changed the original content of the vector.
Therefore, implementations will use the copy constructor of X
to safely transfer the content of the vector into a new storage unless the move constructor is known not to throw, in which case it is safe to move from the previous elements.
With a small change to the definition of X
's move constructor (marking it as noexcept
), in fact, the output of the program is now the expected one.:
struct X
{
X() { }
X(int) { }
X(X const&) { std::cout << "X(X const&)" << std::endl; }
X(X&&) noexcept { std::cout << "X(X&&)" << std::endl; }
// ^^^^^^^^
};
Think about the exception guarantees: If there's an exception during the reallocation, the vector has to remain unchanged. This can only be guaranteed by copying the elements and retaining the old set until the entire copy has succeeded.
Only if you know that the move constructor doesn't throw can you safely move the elements to the new location. To achieve this, declare the move constructor noexcept
.
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