How to Enforce Move Semantics When a Vector Grows

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



Leave a reply



Submit