问题描述
在 Effective Modern C++ 的第 41 条中,Scott Meyers 提到了这种差异及其对就插入而言的就位效率的影响。
我对此有些怀疑,但在提出问题之前,我需要了解这两种添加元素的方式之间的区别。
考虑书中的代码示例:
std::vector<std::string> vs;
// adding some elements to vs
vs.emplace(v.begin(),"xyzzy");
很明显,在// adding some elements to vs
之后,可能是
-
vs.capacity() == vs.size()
或 -
vs.capacity() > vs.size()
。
相应地,
-
vs
必须重新分配,并且vs
中所有预先存在的元素(那些名为vs[0]
、vs[1]
、... 在重新分配发生之前)必须重新分配移动构造到新的内存位置( 的新位置vs[1]
,vs[2]
,...) -
vs
必须vs.resize(vs.size() + 1)
并且所有预先存在的元素都必须移动分配到下一个索引,显然是向后处理,从最后一个元素到第一个元素。
(显然我提到了移动操作,因为 std::string
提供了 noexcept
移动操作。如果不是这种情况,那么上面的场景略有不同,将使用复制操作。)
然而,进入我问题的关键,紧接在上面的代码之后,这本书读到(我的斜体)
[…] 很少有实现会构建添加的std::string
到vs[0]
占用的内存中。相反,他们会移动分配价值到位。 […]
在完成房间以容纳新元素之后,两种情况是什么?
- 如果
emplace
通过移动赋值添加元素,这意味着它v[0] = std::string{strarg};
wherestrarg == "xyzzy"
,对吗? - 但是构造由
v[0]
占据的元素的另一种情况是什么?是展示位置new
吗?它会是什么样子?我想它应该类似于 Placement new here 部分的代码块,但我不确定在这种情况下它会是什么样子。
解决方法
实现 emplace
的方法有很多种,而且标准对于实现必须如何实现非常宽松。
给定一个指向由 std::allocator_traits<allocator_type>::allocate
分配的某处的指针,向量构造新对象的唯一方法是使用 std::allocator_traits<allocator_type>::construct
。对于默认分配器,这将调用placement new。
现在,如果确实发生了重新分配,放置新元素的明显方法是调用 allocator_traits::construct(get_allocator(),ptr,std::forward<Args>(args...))
。这将等同于 new (ptr) std::string("xyzzy")
。但请注意,所有其他元素也通过 allocator_traits::construct(get_allocator(),std::move(old_ptr))
移动构造到新缓冲区。
如果没有发生重新分配,大多数实现只会用 value_type(std::forward<Args>(args...))
构造一个元素并从中移动分配(相当于 v[0] = std::string("xyzzy")
)。这就是 libstdc++ 所做的。
或者,可以通过allocator_traits::destroy
(默认分配器为v[0] = std::string("xyzzy")
)销毁对象,而不是移动构造(&v[0])->~value_type()
,然后可以通过{{1 }}。这似乎更难实现,因为需要特别小心以确保如果移动构造函数抛出该元素不会被破坏两次,这可能就是为什么只有“少数实现”会这样做的原因。
顺便说一句,没有强异常保证,因此不必使用 allocator_traits::construct
,并且移动构造函数可能始终被调用,即使移动构造函数不是 move_if_noexcept
。