Why do you perform deep replication when expanding std::vector?

Introduction

We know that the reason why std::vector can be dynamically expanded while maintaining sequential storage mainly depends on its capacity expansion and replication mechanism. When the capacity is full, it will subdivide a larger memory area, and then copy all the elements.
However, the author found a strange phenomenon. During the expansion of std::vector, the elements in it were deeply copied. See the sample code:

#include <iostream>
#include <vector>

struct Test {
    Test() {std::cout << "Test" << std::endl;}
    ~Test() {std::cout << "~Test" << std::endl;}
    Test(const Test &) {std::cout << "Test copy" << std::endl;}
    Test(Test &&) {std::cout << "Test move" << std::endl;}
};

int main(int argc, const char *argv[]) {
    std::vector<Test> ve;
    ve.emplace_back();
    ve.emplace_back();
    ve.emplace_back();
    return 0;
}

The printing results are as follows:

Test
Test
Test copy
~Test
Test
Test copy
Test copy
~Test
~Test
~Test
~Test
~Test

Since we did not call the reverse function, only one element size was allocated by default. First empty_ Back, only one ordinary construction is performed. Second empty_ Back, you need to expand the capacity, then copy the first element, and then release the original object. Therefore, in addition to a new construction, there is also a copy and release. The following behaviors are similar and will not be repeated,
But the key problem is that the Test class clearly implements the mobile construction (shallow replication), but the copy construction (deep replication) is called here.
If the vector expansion has no brain to call the copy structure, if the object contains many external chain members (such as pointers to buffer, pointers to other objects, etc.), calling the copy structure means that all the linked objects should be reconstructed. This is obviously unnecessary for the vector expansion, which will extremely waste memory space.

Find the reason

Based on the above reasons, I don't think STL developers can even consider this problem, but I can't figure out why I can't call when I clearly implement mobile construction.
With such questions, I studied the source code of STL (GNU version). When the vector is expanded, I will call the _M_realloc_insert function, which is implemented in the vector.tcc file. When copying existing elements in this function, I see codes like this:

__new_finish
		= std::__uninitialized_move_if_noexcept_a
		(__old_start, __position.base(),
		 __new_start, _M_get_Tp_allocator());

	      ++__new_finish;

That's what's interesting__ uninitialized_move_if_noexcept_a. We found the implementation of this function:

template<typename _InputIterator, typename _ForwardIterator,
	   typename _Allocator>
    inline _ForwardIterator
    __uninitialized_move_if_noexcept_a(_InputIterator __first,
				       _InputIterator __last,
				       _ForwardIterator __result,
				       _Allocator& __alloc)
    {
      return std::__uninitialized_copy_a
	(_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__first),
	 _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(__last), __result, __alloc);
    }

Take another look_ GLIBCXX_ MAKE_ MOVE_ IF_ NOEXCEPT_ Implementation of iterator

#if __cplusplus >= 201103L
#define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) std::__make_move_if_noexcept_iterator(_Iter)
#else
#define _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(_Iter) (_Iter)
#endif // C++11

In other words, before C++11, this thing was the object itself (after all, C++11 had not moved the structure before). After C++11, it was defined as _make_move_if_noexcept_iterator. Continue to check its definition.

template<typename _Iterator, typename _ReturnType
    = typename conditional<__move_if_noexcept_cond
      <typename iterator_traits<_Iterator>::value_type>::value,
                _Iterator, move_iterator<_Iterator>>::type>
    inline _GLIBCXX17_CONSTEXPR _ReturnType
    __make_move_if_noexcept_iterator(_Iterator __i)
    { return _ReturnType(__i); }

A conditional is used here to determine the type of the iterator if__ move_ if_ noexcept_ If cond is true, take the iterator itself, otherwise take the mobile iterator. It seems that the problem is here. The Test in our routine must meet this requirement__ move_if_noexcept_cond, resulting in the use of the original iterator.
Keep digging this__ move_if_noexcept_cond, see the following code:

template<typename _Tp>
    struct __move_if_noexcept_cond
    : public __and_<__not_<is_nothrow_move_constructible<_Tp>>,
                    is_copy_constructible<_Tp>>::type { };

That is, if a class does not have a mobile constructor that does not throw exceptions and can be copied, it is true.
The Test class obviously conforms to the, so vector < Test > traverses with an ordinary iterator during replication, and naturally calls the copy constructor for replication.

resolvent

So, we need to make Test not meet__ move_ if_ noexcept_ The condition of cond, that is, the move constructor should be declared as noexcept here, which means that it will not throw an exception. In this way, the vector < Test > will use the move iterator during replication (that is, it will wrap a layer of std::move), so as to trigger the move construct.
Let's also take a look at the principle of mobile iterators:

template<typename _Iterator>
class move_iterator {
    _Iterator _M_current;
    // ...
  public:
    using iterator_type = _Iterator;
	explicit _GLIBCXX17_CONSTEXPR
      	move_iterator(iterator_type __i)
      	: _M_current(std::move(__i)) { }
    // ...
}

std::move is indeed called, which proves that our idea is correct.
Therefore, modify the Test code to realize the noexcept mobile structure:

struct Test {
    long a, b, c, d;
    Test() {std::cout << "Test" << std::endl;}
    ~Test() {std::cout << "~Test" << std::endl;}
    Test(const Test &) {std::cout << "Test copy" << std::endl;}
    Test(Test &&) noexcept {std::cout << "Test move" << std::endl;}
};

int main(int argc, const char *argv[]) {
    std::vector<Test> ve;
    ve.emplace_back();
    ve.emplace_back();
    ve.emplace_back();
    return 0;
}

The printing results are as follows:

Test
Test
Test move
~Test
Test
Test move
Test move
~Test
~Test
~Test
~Test
~Test

This time, as we wish, the mobile construct is called.

conclusion

In STL, exceptions are considered. Therefore, the replication behavior inside containers like this requires that exceptions cannot occur. Therefore, it will be called only when the mobile constructor is declared as noexcept, otherwise the copy constructor will be called uniformly.
However, exceptions should not be thrown in the mobile constructor, so in most cases, the mobile constructor should be declared with noexcept.

Keywords: C++

Added by Mchl on Thu, 30 Dec 2021 09:23:00 +0200