【问题标题】:Can std::vector emplace_back copy construct from an element of the vector itself?std::vector emplace_back 可以从向量本身的元素复制构造吗?
【发布时间】: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

标签: c++ c++11 vector stl


【解决方案1】:

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

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

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

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】:

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

      1. 分配新内存
      2. 放置对象
      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
      猜你喜欢
      • 2014-05-24
      • 1970-01-01
      • 2019-10-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-07
      • 2015-08-23
      相关资源
      最近更新 更多