std :: vector :: erase的MoveAssignable要求与std :: vector :: emplace_back的MoveInsertable要求

时间:2018-03-08 10:34:45

标签: c++ c++11

为什么在使用MoveAssignable时,C ++标准要求元素可移动(std::vector::erase)?为什么不使用移动构造函数(MoveInsertable)对需要移位的元素使用就地构造(又名放置新),就像向{添加元素一样{1}}超出了容器的容量,并在内部分配了一个新的更大的内存块?从概念上讲(imho),向std::vector添加元素和删除元素似乎是双重的,但在容器管理方面却非常相似。因此,有人可以澄清并解释实际使用的不那么相似的操作背后的动机(std::vector的{​​{3}}要求与std::vector::erase的{​​{3}}要求) ?

Imho,MoveAssignable要求非常严格,因为对于大多数对象,我看不到赋值运算符的需要(复制或移动)(在不考虑将这些对象添加到{的可能性时{1}} 的)。此外,这似乎也贬低了const成员变量的使用(从类接口和内部类实现的角度来看都强加了const,而不仅仅是const访问器成员方法,它们只能从类接口的角度强加constness而不是来自内部的类实现。)

2 个答案:

答案 0 :(得分:4)

  

为什么C ++标准在使用std::vector::erase时要求元素可移动(MoveAssignable)?

用于性能和异常安全。

  

为什么不使用移动构造函数(MoveInsertable)来为需要移位的元素使用就地构造(也就是新的布局),就像向std :: vector添加元素超过了容器和内部分配了一个新的更大的内存块?从概念上讲(imho),向std :: vector添加元素和删除元素似乎是双重的,但在容器管理方面却非常相似。

不,不是真的。

删除元素时,您不需要创建任何新对象。您正在严格减少元素的数量,因此不需要调用构造函数。当你添加元素时,显然你需要构造对象(至少是你插入的新对象,如果向量重新分配,那么你需要在新位置构造每个元素)。

  

因此,有人可以澄清并解释实际使用的不那么相似的操作背后的动机(std :: vector :: erase' MoveAssignable要求与std :: vector :: emplace_back' s MoveInsertable要求)?

可能通过销毁受影响的所有元素来实现擦除,但这样做效率会降低。考虑一个vector<X>X管理大块内存,只支持复制,而不是高效移动。如果您销毁每个对象并使用placement new在同一位置重新创建一个新对象,则每个元素已经拥有的内存将被释放,然后再次重新分配新内存。如果每个X拥有的内存块大小相同,则非常浪费:对于释放内存的每个元素,再次分配完全相同的量。如果使用赋值实现它,那么就没有重新分配:元素已经具有所需的存储量,因此它可以将数据从一个元素复制到下一个元素,进入已有的内存。

您还为每个元素运行析构函数构造函数,而不仅仅是赋值运算符。

但更重要的是,如果通过抛出异常来构造新元素失败,那么你将留下一个&#34;洞&#34;在向量的中间。你已经摧毁了一个元素,但没有在它的位置构建一个新元素。这打破了容器的不变量。如果使用赋值,则向量中永远不会有包含死对象的洞。如果赋值抛出,则源和目标都是有效对象,因为还没有运行析构函数。

  Imho,MoveAssignable要求非常严格,因为对于大多数对象,我没有看到需要赋值运算符(复制或移动)(当不考虑将这些对象添加到std :: vector的可能性时)。

我会说,一般来说,如果没有任何其他要求,您的类型应该是可分配的(concrete types should be regular)。对于&#34;价值类型&#34;这当然是正确的。可以存储在容器中。因此,如果要在标准容器中使用类型,则需要对必要的操作进行建模。

可分配性不是一些深奥的奇怪属性,它应该是大多数对象提供的默认行为。不是&#34;常规的类型&#34;应该是例外,而不是常态。

  

此外,这似乎也贬低了const成员变量的使用(从类接口和内部类实现的角度来看,它都强加了const,而不仅仅是const访问器成员方法,它们只能从const角度强加constness。类接口而不是内部类实现。)

const成员函数如何不对数据成员强加const?

答案 1 :(得分:2)

在移动操作之后,仍然需要在内容“移出”的对象上调用析构函数(see this)。

因此,为了调用move构造函数,必须首先在该内存位置调用析构函数。这使得以下操作几乎相同:

版本1:

first->~Element();
new(first) Element(std::move(*second))

和版本2:

*first = std::move(second); 
//inside operator =(&&), `this` object must be destroyed before doing the actual move;

对于版本2,您必须首先销毁现有对象,否则最终会泄漏内存。这基本上就是自己调用析构函数。

关于puch_back和其他重新分配操作,过程略有不同,因为首先调用移动构造函数,然后调用旧向量上的析构函数。这是执行它的“最佳”方式,否则使用赋值将需要空构造函数,移动赋值和(使用内部“析构函数”)和另一个远离“最佳”的析构函数。

从技术上讲,你可以使用版本1进行擦除操作,但它可能不是最佳的,因为用户可以在该任务中获得他想要的任何内容,并且最终可能比单独的析构函数和构造函数执行更少的操作。

人们通常使用版本1实现operator = (T &&),因为它会删除冗余代码。另一种选择是交换技巧(参见boost intrusive_ptr implementation)。

{ Element(std::move(*second)).swap(*first); }
// let's break down what's happening here in order:
// move constructor, swap, destructor on local element(containing original content of `first`)