Подтвердить что ты не робот

Может ли std::vector emplace_back копировать конструкцию из элемента самого вектора?

При использовании push_back of std::vector я могу нажать элемент самого вектора, не опасаясь аннулирования аргумента из-за перераспределения:

std::vector<std::string> v = { "a", "b" };
v.push_back(v[0]); // This is ok even if v.capacity() == 2 before this call.

Однако при использовании emplace_back, std::vector пересылает аргумент конструктору std::string, так что построение копирования происходит на месте в векторе. Это заставляет меня подозревать, что перераспределение вектора происходит до того, как новая строка будет построена копией (иначе она не будет выделена на место), тем самым аннулируя аргумент перед использованием.

Означает ли это, что я не могу добавить элемент самого вектора с помощью emplace_back, или у нас есть какая-то гарантия в случае перераспределения, аналогичная push_back?

В коде:

std::vector<std::string> v = { "a", "b" };
v.emplace_back(v[0]); // Is this valid, even if v.capacity() == 2 before this call?
4b9b3361

Ответ 1

emplace_back требуется, чтобы быть безопасным по той же причине push_back требуется быть безопасным; недействительность указателей и ссылок действует только после возврата вызова метода модификации.

На практике это означает, что emplace_back, выполняющий перераспределение, требуется выполнить в следующем порядке (игнорируя обработку ошибок):

  • Выделить новую емкость.
  • Emplace - построить новый элемент в конце нового сегмента данных
  • Переместить-построить существующие элементы в новый сегмент данных
  • Уничтожить и освободить старый сегмент данных

В этот поток reddit STL подтверждает отказ VC11 поддерживать v.emplace_back(v[0]) как ошибку, поэтому вам обязательно нужно проверить, поддерживает ли ваша библиотека это использование и не воспринимать это как должное.

Обратите внимание, что некоторые формы самонастройки специально запрещены Стандартом; например, в пункте [sequence.reqmts]. Таблица 100 a.insert(p,i,j) имеет необходимое условие: i и j не являются итераторами в a ".

Ответ 2

В отличие от того, что здесь написано несколькими другими людьми, я сделал на этой неделе опыт, что это не безопасно, по крайней мере, при попытке иметь переносимый код с определенным поведением.

Вот пример кода, который может вызывать поведение undefined:

std::vector<uint32_t> v;
v.push_back(0);
// some more push backs into v followed but are not shown here...

v.emplace_back(v.back()); // problem is here!

Приведенный выше код работает под Linux с g++ STL без проблем.

При запуске того же кода в Windows (скомпилированном с обновлением Visual Studio 2013 Update5) вектор иногда содержит некоторые искаженные элементы (по-видимому, случайные значения).

Причина в том, что ссылка, возвращаемая v.back(), была признана недействительной из-за того, что контейнер достиг своего предела емкости внутри v.emplace_back(), прежде чем элемент был добавлен в конце.

Я рассмотрел реализацию VС++ STL emplace_back() и, как представляется, выделил новое хранилище, скопировал поверх существующих векторных элементов в новое хранилище, освободил старое хранилище и затем создал элемент в конце нового хранилища. В этот момент ссылочный элемент, лежащий в основе памяти, может быть уже освобожден или иным образом недействителен. Это создавало поведение undefined, в результате чего векторные элементы, вставленные в пороги перераспределения, искажались.

Кажется, что это (все еще нефиксированная) ошибка в Visual Studio. С другими реализациями STL, которые я пытался, проблема не возникала.

В конце вы должны избегать передачи ссылки на векторный элемент на тот же вектор emplace_back(), по крайней мере, если ваш код будет скомпилирован с Visual Studio и должен работать.

Ответ 3

Я проверил свою реализацию вектора, и он работает здесь следующим образом:

  • Выделить новую память
  • Объект Emplace
  • Последняя память Dealloc

Итак, все в порядке. Аналогичная реализация используется для push_back, так что это один штраф два.

FYI, вот важная часть реализации. Я добавил комментарии:

template<typename _Tp, typename _Alloc>
    template<typename... _Args>
      void
      vector<_Tp, _Alloc>::
      _M_emplace_back_aux(_Args&&... __args)
      {
    const size_type __len =
      _M_check_len(size_type(1), "vector::_M_emplace_back_aux");
// HERE WE DO THE ALLOCATION
    pointer __new_start(this->_M_allocate(__len));
    pointer __new_finish(__new_start);
    __try
      {
// HERE WE EMPLACE THE ELEMENT
        _Alloc_traits::construct(this->_M_impl, __new_start + size(),
                     std::forward<_Args>(__args)...);
        __new_finish = 0;

        __new_finish
          = std::__uninitialized_move_if_noexcept_a
          (this->_M_impl._M_start, this->_M_impl._M_finish,
           __new_start, _M_get_Tp_allocator());

        ++__new_finish;
      }
    __catch(...)
      {
        if (!__new_finish)
          _Alloc_traits::destroy(this->_M_impl, __new_start + size());
        else
          std::_Destroy(__new_start, __new_finish, _M_get_Tp_allocator());
        _M_deallocate(__new_start, __len);
        __throw_exception_again;
      }
    std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
              _M_get_Tp_allocator());
// HERE WE DESTROY THE OLD MEMORY
    _M_deallocate(this->_M_impl._M_start,
              this->_M_impl._M_end_of_storage
              - this->_M_impl._M_start);
    this->_M_impl._M_start = __new_start;
    this->_M_impl._M_finish = __new_finish;
    this->_M_impl._M_end_of_storage = __new_start + __len;
      }