提问人:KnowItAllWannabe 提问时间:8/14/2014 最后编辑:Jan SchultkeKnowItAllWannabe 更新时间:10/3/2023 访问量:1537
为什么在声明移动操作时删除复制操作?
Why are copy operations deleted when move operations are declared?
问:
当类显式声明复制操作(即复制构造函数或复制赋值运算符)时,不会为该类声明移动操作。但是,当类显式声明移动操作时,复制操作将声明为已删除。为什么存在这种不对称性?为什么不直接指定如果声明了移动操作,则不会声明任何复制操作?据我所知,不会有任何行为差异,也不需要对移动和复制操作进行不对称处理。
[对于喜欢引用该标准的人,12.8/9 和 12.8/20 中指定了具有复制操作声明的类缺少移动操作声明的声明,而 12.8/7 和 12.8/18 中指定了具有移动操作声明的类的删除复制操作。
答:
当一个类将被移动但由于没有声明移动构造函数而移动时,编译器将回退到复制构造函数。在同样的情况下,如果将移动构造函数声明为已删除,则程序的格式不正确。因此,如果将移动构造函数隐式声明为已删除,则涉及现有 C++11 之前类的许多合理代码将无法编译。诸如此类的东西myVector.push_back(MyClass())
这就解释了为什么在定义复制构造函数时不能隐式声明删除移动构造函数。这就留下了一个问题,即在定义移动构造函数时,为什么会隐式声明 copy 构造函数 delete。
我不知道委员会的确切动机,但我有一个猜测。如果向现有 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 构造函数的存在是为了抑制 copy 构造函数的隐式声明,那么 (1) 处一段看似无关的代码仍将编译,但会默默地改变其含义:它现在将调用 和 。那会很糟糕。operator int()
C(int)
另一方面,如果复制构造函数被隐式声明为已删除,那么 (1) 将无法编译,提醒我注意这个问题。我会检查情况并决定我是否仍然需要一个默认的复制构造函数;如果是这样,我会补充C(const C&)=default;
评论
它本质上是为了避免迁移代码执行意外的不同操作。
复制和移动需要一定程度的连贯性,因此 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,则默认移动将执行复制(因此最终行为与 C++03 相同)。
如果声明了 copy,则删除 move,并且正弦衰减为 第三个语句会导致来自临时的副本。这仍然是 C++03 相同的行为。C&&
C const&
现在,如果我稍后添加一个移动 ctor,这意味着我正在改变 C 的行为(在 C++03 中定义 C 时没有计划),并且由于可移动对象不需要可复制(反之亦然),编译器假设通过使其可移动,dafault 副本可能不再足够。由我来实施它与移动保持一致,或者 - 如果我发现它足够 - 恢复C(const C&)=default;
评论
C&&
不会“衰减”为 ,但可以绑定到 rvalues。(衰变已经意味着不同的东西 w.r.t 参考)C const&
C const&
=delete
为什么存在这种不对称性?
向后兼容,并且因为复制和移动之间的关系已经不对称。MoveConstructible 的定义是 CopyConstructible 的较弱形式(不要求源对象保持不变),这意味着所有 CopyConstructible 类型也是 MoveConstructible 类型。这是真的,因为采用对 const 的引用的复制构造函数将处理右值和左值。
可复制类型可以在没有移动构造函数的情况下从右值初始化(它可能不如使用移动构造函数那样高效)。
复制构造函数还可用于在移动基子对象时在派生类的隐式定义的移动构造函数中执行“移动”。
因此,复制构造函数可以看作是“退化的移动构造函数”,因此,如果一个类型具有复制构造函数,则严格来说它不需要移动构造函数,它已经是 MoveConstructible,因此简单地不声明移动构造函数是可以接受的。
反之则不然,可移动类型不一定是可复制的,例如仅移动类型。在这些情况下,删除复制构造函数和赋值比不声明它们并获得有关将左值绑定到右值引用的错误提供更好的诊断。
为什么不直接指定如果声明了移动操作,则不会声明任何复制操作?
更好的诊断和更明确的语义。“定义为已删除”是 C++11 明确表示“不允许此操作”的方式,而不是碰巧被错误地省略或由于其他原因而丢失。
移动构造函数和移动赋值运算符的“未声明”的特殊情况是不寻常的,并且由于上述不对称性而具有特殊性,但通常最好将特殊情况保留在少数狭窄的情况下(这里值得注意的是,“未声明”也可以应用于默认构造函数)。
另外值得注意的是,你提到的一段,[class.copy] p7,说(强调我的):
如果类定义未显式声明复制构造函数,则隐式声明一个复制构造函数。如果类定义声明移动构造函数或移动赋值运算符,则隐式声明的复制构造函数定义为已删除;否则,它被定义为默认 (8.4)。如果类具有用户声明的复制赋值运算符或用户声明的析构函数,则不推荐使用后一种情况。
“后一种情况”是指“否则,它被定义为默认”的部分。第18段对复制分配操作者也有类似的措辞。
因此,委员会的意图是,在未来的C++版本中,其他类型的特殊成员函数也会导致复制构造函数和复制赋值运算符被删除。理由是,如果你的类需要一个用户定义的析构函数,那么隐式定义的复制行为可能不会做正确的事情。出于向后兼容性的原因,尚未对 C++11 或 C++14 进行更改,但这个想法是,在未来的某个版本中,为了防止复制构造函数和复制赋值运算符被删除,您需要显式声明它们并将它们定义为默认值。
因此,如果复制构造函数可能没有做正确的事情,则删除它们通常是一般情况,而“未声明”是移动构造函数的特例,因为复制构造函数无论如何都可以提供退化的移动。
评论