How to enable move semantics when adding custom objects to a vector?
Your Test
struct does not define any special member functions (copy constructor, destructor, etc.) That means a default move assignment operator and a default move copy constructor are generated automatically, and they will move each data member of the struct. So Test
is a movable type, and it benefits from that since vector<size_t>
is a movable data member.
However, moves are not performed automatically because moving from an object changes it. Even though you'd think that this:
vecOfTest.push_back(test);
}
would do an implicit move because the scope ends, it will not. Implicit moves would put both the compiler and the programmer in a difficult situation. The compiler would be required to prove that invalidating test
is OK. The programmer would be required to constantly investigate whether or not an explicit move is needed or not, and the end result of that would be to just do explicit moves anyway. So for that reason, implicit moves do not happen (but see below for the exception to the rule.) You need to do it yourself:
vecOfTest.push_back(std::move(test));
The only case where you need to not move is when the move would interfere with elision. For example, in a function that returns a Test
, this:
Test test;
return std::move(test);
would move, but it's better not to. It's better to:
return test;
instead. This is not an implicit move. It's an elision. Elision is faster than move, and doing a move would prevent elision. However, in cases where elision is not possible, then an implicit move is performed. This is the only case I know of where an implicit move will happen: as a substitute for elision. Your original code:
vecOfTest.push_back(test);
is not a case for elision, and so an implicit move will never happen.
How does move semantics apply on the following snippet when no xvalue is present?
You're correct that object x
won't be moved from. The move operations gaining performance have to do with the other k
vectors already in V
.
As a vector grows (unless reserve
was used with a sufficient size), it will sometimes need to reallocate to get a bigger chunk of memory, since its elements are required to be in contiguous memory. This doesn't happen on every push_back
, but it will certainly happen sometimes in this example. So let's say push_back
and other functions make use of some private function grow_capacity
, which gets enough memory, and then creates objects already in the vector within that memory.
In C++03, the only reasonable way to create the objects in the new memory, for an arbitrary template parameter T
, is using the copy constructor of T
.
// C++03 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
T* new_data = get_allocator().allocate(new_capacity);
T* new_end = new_data;
try {
for (const_iterator iter = begin(); iter != end(); ++iter) {
::new(static_cast<void*>(new_end)) T(*iter); // T copy ctor!
++new_end;
}
} catch (...) {
while (new_end != new_data) (--new_end)->~T();
get_allocator().deallocate(new_data, new_capacity);
throw;
}
// Clean up old objects and memory.
for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
riter->~T();
get_allocator().deallocate(_data, _capacity);
// Assign private members.
_data = new_data;
_capacity = new_capacity;
}
In C++11 and later, when std::vector<T>
needs to reallocate to a larger capacity, it is allowed to move its T
elements instead of copying them if it can do so without breaking the strong exception guarantee. This requires that the move constructor is declared to not throw any exceptions. But if the move constructor might throw, the elements need to be copied in the old way, to make sure the vector will remain in a consistent state if that happens.
// C++17 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
T* new_data = get_allocator().allocate(new_capacity);
if constexpr (::std::is_nothrow_move_constructible_v<T>) {
::std::uninitialized_move(begin(), end(), new_data); // T move ctor!
} else {
T* new_end = new_data;
try {
for (const T& old_obj : *this) {
::new(static_cast<void*>(new_end)) T(old_obj); // T copy ctor!
++new_end;
}
} catch (...) {
while (new_end != new_data) (--new_end)->~T();
get_allocator().deallocate(new_data, new_capacity);
throw;
}
}
for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
riter->~T();
get_allocator().deallocate(_data, _capacity);
// Assign private members.
_data = new_data;
_capacity = new_capacity;
}
So in the container with type std::vector<std::vector<int> >
, T
is std::vector<int>
. Growing the capacity the C++03 way will sometimes require a large number of copy constructors then destructors for the std::vector<int>
. Each copy constructor allocates some memory and copies 1000 int
values, and each destructor deallocates some memory, so this will really add up. But with the C++11 std::vector
, since the element type std::vector<int>
does have a noexcept
move constructor, the std::vector<std::vector<int>>
container can just use that move constructor, which is just a few swaps of scalar members and also causes the destructors of the moved-from old objects to do nothing.
vector::push_back using std::move for internal reallocations
According to [vector.modifiers]/2 if the element type of std::vector
is not copy-insertable and not nothrow-move-constructible, then an exception thrown from a move constructor of the element type results in an unspecified state of the container.
std::vector
must prefer the copy constructor if the type isn't nothrow-movable, so that the exception guarantees can be preserved, but if that isn't possible, it is impossible to give strong exception guarantees.
Related Topics
Is There a Performance Difference Between I++ and ++I in C++
How to Create a Contiguous 2D Array in C++
What Does Int Argc, Char *Argv[] Mean
How to Convert a Std::String to Const Char* or Char*
What Are the Differences Between Struct and Class in C++
What Is a "Translation Unit" in C++
Why Can't Variables Be Declared in a Switch Statement
How to Get the List of Files in a Directory Using C or C++
"Unpacking" a Tuple to Call a Matching Function Pointer
Is Segmentation Fault Actual Undefined Behavior When We Refer to a Non-Static Data-Member
How to Parse a String to an Int in C++
What Is the Curiously Recurring Template Pattern (Crtp)
Convert Char to Int in C and C++
C++ Returning Reference to Local Variable