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_back
或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?
【问题讨论】:
什么是“害怕由于重新分配而使论点无效”? @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)
具有先决条件“i
和 j
不是 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()处的另一个地址?