std::vector emplace_back 可以从向量本身的元素复制构造吗?

Posted

技术标签:

【中文标题】std::vector emplace_back 可以从向量本身的元素复制构造吗?【英文标题】:Can std::vector emplace_back copy construct from an element of the vector itself? 【发布时间】:2014-09-14 12:47:50 【问题描述】:

当使用push_backstd::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?

【问题讨论】:

什么是“害怕由于重新分配而使论点无效”? @xylosper 如果您处于向量容量的限制,push_back/emplace_back 将重新分配向量以增加其大小。如果发生重新分配,则对该向量的元素和迭代器的引用无效。在这种情况下, push_back 的参数在使用之前可能会失效。然而,事实证明 std::vector 跳过了箍,因此这对 push_back 来说不是问题。 我认为理论上是可以保证的,但在实践中你可能会遇到它作为一个错误,所以你应该尽量避免它。 @rasmus:我认为与push_back 案例完全相同的推理适用:没有什么限制这个论点,因此它很可能来自同一个向量 这个问题通过emplace变得更有趣:cplusplus.github.io/LWG/lwg-active.html#2164 【参考方案1】:

emplace_back 必须是安全的,原因与push_back is required to be safe 相同;指针和引用的失效只有在修改方法调用返回时才会生效。

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

    分配新容量 Emplace-在新数据段末尾构造新元素 将现有元素移动到新的数据段中 销毁和释放旧数据段

this reddit thread STL 承认 VC11 无法支持 v.emplace_back(v[0]) 是一个错误,所以你一定要检查你的库是否支持这种用法,不要认为这是理所当然的。

请注意,标准明确禁止某些形式的自插入;例如,在 [sequence.reqmts] 第 4 段中,表 100 a.insert(p,i,j) 具有先决条件“ij 不是 a 的迭代器”。

【讨论】:

【参考方案2】:

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

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

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!

上面的代码在带有 g++ STL 的 Linux 上运行没有问题。

在 Windows 上运行相同的代码(使用 Visual Studio 2013 Update5 编译)时,向量有时会包含一些乱码元素(看似随机的值)。

原因是v.back() 返回的引用由于容器在v.emplace_back() 内达到其容量限制,在最后添加元素之前无效。

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

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

最后,您现在应该避免将对向量元素的引用传递给同一向量的emplace_back(),至少在您的代码使用 Visual Studio 编译并且应该可以工作的情况下。

【讨论】:

【参考方案3】:

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

    分配新内存 放置对象 释放旧内存

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

仅供参考,这是实现的相关部分。我已经添加了 cmets:

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;
      

【讨论】:

对向量本身的元素执行 push_back 是非常安全的。参见例如this link。

以上是关于std::vector emplace_back 可以从向量本身的元素复制构造吗?的主要内容,如果未能解决你的问题,请参考以下文章

即使根据容量()仍有未使用的空间,std::vector 能否将其数据移动到 emplace_back()处的另一个地址?

C++的emplace_back函数介绍

std::emplace_back 调用向量中其他对象的构造函数?

C++ 二维 map vector 赋值 遍历 实例 降序

C++ 二维 map vector 赋值 遍历 实例 降序

为啥 emplace_back() 不使用统一初始化?