为什么在声明移动操作时删除复制操作?

Why are copy operations deleted when move operations are declared?

本文关键字:操作 删除 复制 为什么 声明 移动      更新时间:2023-10-16

当类显式声明复制操作(即复制构造函数或复制赋值操作符)时,不为该类声明move操作。但是,当类显式声明移动操作时,将复制操作声明为已删除。为什么这种不对称会存在?为什么不直接指定如果声明了移动操作,就不声明复制操作呢?据我所知,不会有任何行为差异,也不需要对移动和复制操作进行不对称处理。

[对于喜欢引用标准的人来说,在12.8/9和12.8/20中规定了对有copy操作声明的类不声明move操作,在12.8/7和12.8/18中规定了对有move操作声明的类删除copy操作。

当一个类要被移动,但没有声明移动构造函数时,编译器会返回到复制构造函数。在同样的情况下,如果move构造函数声明为deleted,则程序将是病态的。因此,如果move构造函数被隐式声明为deleted,那么涉及现有c++ 11之前的类的大量合理代码将无法编译。比如myVector.push_back(MyClass())

这解释了为什么在定义复制构造函数时不能隐式声明move构造函数为deleted。这就留下了一个问题:为什么在定义move构造函数时隐式声明copy构造函数为deleted ?

我不知道委员会的确切动机,但我有一个猜测。如果将move构造函数添加到现有的c++ 03风格的类中是为了删除(先前隐式定义的)复制构造函数,那么使用该类的现有代码可能会以微妙的方式改变含义,因为重载解析会选择意外的重载,这些重载过去被视为较差的匹配而被拒绝。

考虑:

struct C {
  C(int) {}
  operator int() { return 42; }
};
C a(1);
C b(a);  // (1)

这是一个遗留的c++ 03类。(1)调用(隐式定义的)复制构造函数。C b((int)a);也是可行的,但匹配效果较差。

假设,出于某种原因,我决定向该类添加一个显式move构造函数。如果move构造函数的存在抑制了复制构造函数的隐式声明,那么(1)中看似不相关的一段代码仍然可以编译,但默默地改变了它的含义:它现在将调用operator int()C(int)。那可不好。

另一方面,如果将复制构造函数隐式声明为deleted,则(1)将无法编译,从而提醒我注意这个问题。我会检查情况并决定是否仍然需要默认复制构造函数;如果是这样,我会添加C(const C&)=default;

为什么这种不对称存在?

向后兼容性,因为复制和移动之间的关系已经是不对称的。MoveConstructible的定义是CopyConstructible的一个特例,这意味着所有的CopyConstructible类型也是MoveConstructible类型。这是因为接受const引用的复制构造函数既可以处理左值,也可以处理右值。

可复制类型可以从右值初始化,而不需要使用move构造函数(它可能不如使用move构造函数有效)。

复制构造函数还可以用于在派生类的隐式定义的移动构造函数中移动基子对象时执行"移动"。

所以复制构造函数可以看作是"简并的移动构造函数",所以如果一个类型有复制构造函数,它并不严格地需要移动构造函数,它已经是MoveConstructible了,所以简单地不声明移动构造函数是可以接受的。

反之则不成立,可移动的字体不一定是可复制的,例如,只能移动的字体。在这些情况下,删除复制构造函数和赋值可以提供更好的诊断,而不仅仅是不声明它们并在将左值绑定到右值引用时产生错误。

为什么不直接指定如果声明了移动操作,就不声明复制操作呢?

更好的诊断和更明确的语义。"定义为删除"是c++ 11明确表示"不允许此操作"的方式,而不是由于错误或其他原因而碰巧被省略。

move构造函数和move赋值操作符的"未声明"的特殊情况是不寻常的,因为上面描述的不对称,但特殊情况通常最好保留在一些狭窄的情况下(这里值得注意的是,"未声明"也可以适用于默认构造函数)。

同样值得注意的是,你提到的段落之一,[class。p7,说(强调我的):

如果类定义没有显式声明复制构造函数,则隐式声明。如果类定义声明了move构造函数或move赋值操作符,则隐式声明的复制构造函数定义为deleted;否则,它被定义为默认值(8.4)。如果类具有用户声明的复制赋值操作符或用户声明的析构函数,则不建议使用后一种情况。

"后一种情况"指的是"否则,它被定义为默认的"部分。第18段对拷贝赋值操作符有类似的措辞。

因此,委员会的意图是,在将来的c++版本中,其他类型的特殊成员函数也会导致复制构造函数和复制赋值操作符被删除。原因是,如果您的类需要用户定义的析构函数,那么隐式定义的复制行为可能不会做正确的事情。由于向后兼容性的原因,c++ 11或c++ 14没有做这个更改,但其想法是,在将来的某个版本中,为了防止复制构造函数和复制赋值操作符被删除,您需要显式声明它们并将它们定义为默认值。

所以删除复制构造函数是一般情况,而"未声明"是移动构造函数的特殊情况,因为复制构造函数无论如何都可以提供简并移动。

本质上是为了避免迁移后的代码执行意想不到的不同操作。

复制和移动需要一定程度的一致性,所以c++ 11——如果你只声明一个——禁止另一个。

考虑一下:

C a(1); //init
C b(a); //copy
C c(C(1)); //copy from temporary (03) or move(11).

假设你用c++ 03写这个。

假设我稍后在c++ 11中编译它。如果没有声明ctor,则默认的move执行复制操作(因此最终行为与c++ 03相同)。

如果声明了copy,则删除move,并且由于C&&衰变为C const&,第三条语句导致从临时对象复制。这仍然是c++ 03相同的行为。

现在,如果我稍后添加一个move actor,这意味着我正在改变C的行为(在c++ 03中定义C时没有计划的事情),并且由于可移动对象不需要可复制(反之亦然),编译器认为通过使其可移动,默认副本可能不再足够。由我来执行它与移动的一致性,或者-如果我发现它足够-恢复C(const C&)=default;