1

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};where strarg == "xyzzy",对吗?
  • 但是构造被 占据的元素的另一种情况是v[0]什么?是安置new吗?它会是什么样子?我想它应该类似于Placement new here部分的代码块,但我不确定在这种情况下它会是什么样子。
4

1 回答 1

1

有许多不同的实现方式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(), ptr, std::move(old_ptr)).

如果没有发生重新分配,大多数实现将只是构造一个元素value_type(std::forward<Args>(args...))并从中移动分配(相当于v[0] = std::string("xyzzy"))。这就是 libstdc++ 所做的。

v[0] = std::string("xyzzy")或者,可以通过allocator_traits::destroy(对于默认分配器)销毁对象,而不是移动构造,(&v[0])->~value_type()然后可以通过就地构造它allocator_traits::construct。这似乎更难实现,因为如果移动构造函数抛出,需要特别注意确保元素不会被破坏两次,这可能就是为什么只有“少数实现”会这样做。

顺便说一句,没有强大的异常保证,因此move_if_noexcept不必使用并且可以始终调用移动构造函数,即使移动构造函数不是noexcept

于 2021-01-10T09:00:49.357 回答