【问题标题】:Why are copy operations deleted when move operations are declared?为什么声明移动操作时会删除复制操作?
【发布时间】:2014-10-07 14:34:40
【问题描述】:

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

[对于喜欢引用标准的人,12.8/9和12.8/20中规定了对有复制操作声明的类缺少移动操作声明,对有移动操作声明的类规定了删除复制操作在 12.8/7 和 12.8/18 中。]

【问题讨论】:

    标签: c++ c++11 copy-constructor move-semantics


    【解决方案1】:

    当一个类被移动但没有声明移动构造函数时,编译器回退到复制构造函数。在同样的情况下,如果将移动构造函数声明为已删除,则程序将是非良构的。因此,如果将移动构造函数隐式声明为已删除,则许多涉及现有 C++11 之前类的合理代码将无法编译。类似myVector.push_back(MyClass())

    这解释了为什么在定义复制构造函数时不能隐式声明删除移动构造函数。这就留下了为什么复制构造函数在定义移动构造函数时被隐式声明为删除的问题。

    我不知道委员会的确切动机,但我有一个猜测。如果向现有 C++03 风格的类添加移动构造函数是为了删除(以前隐式定义的)复制构造函数,那么使用此类的现有代码可能会以微妙的方式改变含义,因为重载解决方案选择了以前的意外重载被拒绝为更差的匹配项。

    考虑:

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

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

    想象一下,无论出于何种原因,我决定为这个类添加一个显式的移动构造函数。如果移动构造函数的存在是为了抑制复制构造函数的隐式声明,那么 (1) 处看似无关的一段代码仍然可以编译,但会默默地改变其含义:它现在将调用 operator int()C(int)。那会很糟糕。

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

    【讨论】:

    • 如果一个对象将被复制,但由于没有声明复制构造函数,无论是否定义了移动构造函数,它都不会被移动,因为左值不能绑定到右值引用。跨度>
    • 但是如果没有声明复制操作,代码将无法编译。这与我们删除它们时的行为相同。因此我的问题是:为什么要删除复制操作而不是不声明它们?
    • @RobertAllanHenniganLeahy:是的。我想出了另一种解释。
    • 很好的解释和示例,但它与您编辑掉的答案确实不同。除非 SO 不允许,否则我认为最好写第二个答案。无论如何,我对此表示赞同,除非有人提出更好的建议,否则我会将其标记为答案。
    • @KnowItAllWannabe:被编辑掉的答案不准确/具有误导性。复制操作永远不会退回到移动操作。引用绑定和值类别可防止这种情况发生,因此这不是问题行为的正当理由。
    【解决方案2】:

    为什么会存在这种不对称性?

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

    可复制类型可以在没有移动构造函数的情况下从右值初始化(它可能不如使用移动构造函数的效率高)。

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

    所以拷贝构造函数可以看成是“退化的移动构造函数”,所以如果一个类型有拷贝构造函数它并不严格需要移动构造函数,它已经是MoveConstructible,所以简单不声明移动构造函数是可以接受的。

    反之则不然,活字不一定是可复制的,例如只移动类型。在这些情况下,删除复制构造函数和赋值提供了更好的诊断,而不是仅仅不声明它们并得到关于将左值绑定到右值引用的错误。

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

    更好的诊断和更明确的语义。 “定义为已删除”是 C++11 中明确表示“不允许此操作”的方式,而不是只是碰巧被错误省略或因其他原因而遗漏。

    移动构造函数和移动赋值运算符的“未声明”的特殊情况是不寻常的,并且由于上述不对称性而很特殊,但特殊情况通常最好保留在一些狭窄的情况下(这里值得注意的是“不声明”也可以应用于默认构造函数)。

    另外值得注意的是,您提到的段落之一,[class.copy] p7,说(强调我的):

    如果类定义没有显式声明复制构造函数,则隐式声明。如果类定义声明了移动构造函数或移动赋值运算符,则隐式声明的复制构造函数定义为已删除;否则,它被定义为默认值(8.4)。 如果类具有用户声明的复制赋值运算符或用户声明的析构函数,则不推荐使用后一种情况。

    “后一种情况”是指“否则定义为默认”部分。第 18 段对复制赋值运算符有类似的措辞。

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

    因此,如果复制构造函数可能没有做正确的事情,则删除它们是一般情况,而“未声明”是移动构造函数的特殊情况,因为复制构造函数无论如何都可以提供退化移动。

    【讨论】:

      【解决方案3】:

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

      复制和移动需要一定程度的连贯性,因此 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 相同)。

      如果声明了复制,则删除移动,正弦C&& 衰减为C const& 第三个语句导致从临时复制。这仍然是与 C++03 相同的行为。

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

      【讨论】:

      • C&& 不会“衰减”为C const&,但C const& 可以绑定到右值。 (衰减已经意味着不同的 w.r.t 引用)
      • 您会说英语还是只会说Lawyerish?
      • 我的英语说得很好,谢谢,但是这个问题被标记为c++ 并且“decay”在 C++ 中具有特定的含义,所以用它来表示不同的东西可能会让人感到困惑。你可以通过避免混淆术语来改进你的答案,或者你可能会被嘲笑。
      • P.S.您只解释了为什么提供副本会禁用移动,反之亦然,但问题是为什么一个被 =delete 禁用而另一个被未声明而禁用......你没有触及。
      • 这是什么? “最长的小便环境”?
      猜你喜欢
      • 1970-01-01
      • 2021-09-05
      • 1970-01-01
      • 2011-11-19
      • 2013-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多