为什么在使用MoveAssignable
时,C ++标准要求元素可移动(std::vector::erase
)?为什么不使用移动构造函数(MoveInsertable
)对需要移位的元素使用就地构造(又名放置新),就像向{添加元素一样{1}}超出了容器的容量,并在内部分配了一个新的更大的内存块?从概念上讲(imho),向std::vector
添加元素和删除元素似乎是双重的,但在容器管理方面却非常相似。因此,有人可以澄清并解释实际使用的不那么相似的操作背后的动机(std::vector
的{{3}}要求与std::vector::erase
的{{3}}要求) ?
MoveAssignable
要求非常严格,因为对于大多数对象,我看不到赋值运算符的需要(复制或移动)(在不考虑将这些对象添加到{的可能性时{1}} 的)。此外,这似乎也贬低了const成员变量的使用(从类接口和内部类实现的角度来看都强加了const,而不仅仅是const访问器成员方法,它们只能从类接口的角度强加constness而不是来自内部的类实现。)
答案 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`)