可以std::vector emplace_back从vector本身的元素复制构造函数

Can std::vector emplace_back copy construct from an element of the vector itself?

本文关键字:vector 元素 复制 构造函数 back std emplace 可以      更新时间:2023-10-16

当使用std::vectorpush_back时,我可以压入向量本身的一个元素,而不必担心由于重新分配而使参数无效:

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的构造函数,以便在vector中进行复制构造。这使我怀疑vector的重新分配发生在新字符串复制构造之前(否则它不会被分配到位),从而在使用前使实参无效。

这是否意味着我不能用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?

emplace_back必须是安全的,原因与push_back必须是安全的相同;指针和引用的无效只在修改方法调用返回时生效。

在实践中,这意味着emplace_back执行重新分配需要按照以下顺序进行(忽略错误处理):

  1. 分配新容量
  2. 在新数据段
  3. 的末尾放置-构造新元素
  4. 移动-构建现有元素到新的数据段
  5. 销毁旧数据段并释放

在这个reddit线程STL承认VC11支持v.emplace_back(v[0])的失败是一个bug,所以你应该明确检查你的库是否支持这种用法,而不是想当然。

注意某些形式的自插入标准特别禁止的;例如[sequence]。表100 a.insert(p,i,j)有一个先决条件" ij都不是进入a 的迭代器"。

与其他一些人在这里写的相反,本周我的经验是,这是不安全的,至少在尝试具有定义行为的可移植代码时是这样。

下面是一些可能暴露未定义行为的示例代码:

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()内的容器达到其容量限制而无效。

我研究了vc++的emplace_back()的STL实现,它似乎分配了新的存储空间,将现有的向量元素复制到新的存储位置,释放旧的存储空间,然后在新存储空间的末尾构造元素。此时,被引用元素的底层内存可能已经被释放或无效。这会产生未定义的行为,导致在重新分配阈值处插入的向量元素被乱码。

这似乎是Visual Studio中的一个(仍未修复)bug。对于我尝试的其他STL实现,没有发生问题。

最后,你现在应该避免将对vector元素的引用传递给同一个vector的emplace_back(),至少如果你的代码在Visual Studio中编译并且应该工作的话。

我检查了我的矢量实现,它在这里工作如下:

  1. 分配新内存
  2. <
  3. 安置对象/gh>
  4. 释放旧内存

所以这里一切都很好。push_back也使用了类似的实现,所以这个是很好的两个。

供参考,这是实现的相关部分。我添加了注释:

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;
      }