【问题标题】:Rule-of-Three becomes Rule-of-Five with C++11? [closed]C++11 的三规则变成五规则? [关闭]
【发布时间】:2011-06-14 13:01:09
【问题描述】:

所以,在观看了this wonderful lecture 的右值引用之后,我认为每个类都会受益于这样的“移动构造函数”template<class T> MyClass(T&& other)edit,当然还有一个“移动赋值运算符”,template<class T> MyClass& operator=(T&& other) Philipp 在他的回答中指出,如果它具有动态分配的成员,或者通常存储指针。就像你应该有一个copy-ctor,赋值运算符和析构函数,如果前面提到的点适用的话。 想法?

【问题讨论】:

标签: c++ constructor c++11 rvalue-reference rule-of-three


【解决方案1】:

我会说三法则变成三四五法则:

每个类都应该明确定义一个 以下一组特殊成员 功能:

  • 析构函数、复制构造函数、复制赋值运算符

此外,每个显式定义析构函数的类都可以显式定义移动构造函数和/或移动赋值运算符。

通常是以下一组特殊成员之一 功能是明智的:

  • 无(对于隐式生成的特殊成员函数正确且快速的许多简单类)
  • 析构函数、复制构造函数、复制赋值运算符(在本例中为 类将不可移动)
  • 析构函数、移动构造函数、移动赋值运算符(在这种情况下,类将不可复制,对于基础资源不可复制的资源管理类很有用)
  • 析构函数、复制构造函数、复制赋值运算符、移动构造函数(由于复制省略,如果复制赋值运算符按值获取其参数,则没有开销)
  • 析构函数、复制构造函数、复制赋值运算符、移动构造函数、 移动赋值运算符

注意:

  • 对于显式声明任何其他特殊成员函数(如析构函数或复制构造函数或移动赋值运算符)的类,不会生成移动构造函数和移动赋值运算符。
  • 不会为显式声明移动构造函数或移动赋值运算符的类生成复制构造函数和复制赋值运算符。
  • 并且具有显式声明的析构函数和隐式定义的复制构造函数或隐式定义的复制赋值运算符的类被视为已弃用。

特别是以下完全有效的 C++03 多态基类:

class C {
  virtual ~C() { }   // allow subtype polymorphism
};

应该改写如下:

class C {
  C(const C&) = default;               // Copy constructor
  C(C&&) = default;                    // Move constructor
  C& operator=(const C&) = default;  // Copy assignment operator
  C& operator=(C&&) = default;       // Move assignment operator
  virtual ~C() { }                     // Destructor
};

有点烦人,但可能比替代方案更好(在这种情况下,自动生成用于复制的特殊成员函数,没有移动的可能性)。

与三巨头的规则相反,不遵守规则会导致严重的损害,不明确声明移动构造函数和移动赋值运算符通常很好,但在效率方面往往不是最佳的。如上所述,移动构造函数和移动赋值运算符只有在没有显式声明的复制构造函数、复制赋值运算符或析构函数时才会生成。这在复制构造函数和复制赋值运算符的自动生成方面与传统的 C++03 行为不对称,但更安全。因此,定义移动构造函数和移动赋值运算符的可能性非常有用,并创造了新的可能性(纯可移动类),但遵循 C++03 三巨头规则的类仍然可以。

对于资源管理类,如果无法复制基础资源,您可以将复制构造函数和复制赋值运算符定义为已删除(视为定义)。通常您仍然需要移动构造函数和移动赋值运算符。复制和移动赋值运算符通常使用swap 实现,如在 C++03 中。谈swap;如果我们已经有一个移动构造函数和移动赋值运算符,specializing std::swap 将变得不重要,因为通用std::swap 使用移动构造函数和移动赋值运算符(如果可用)(这应该足够快)。

不用于资源管理(即没有非空析构函数)或子类型多态性(即没有虚拟析构函数)的类不应声明五个特殊成员函数中的任何一个;它们都将自动生成,并且行为正确且快速。

【讨论】:

  • @Philipp:嗯,对...如果我做对了,如果您只实现 move-ctor,则传递值赋值运算符“其他”将被移动构造?我认为剩下的指针副本和分配只会被编译器优化......
  • @Xeo:我相信如果类不可复制,那么即使可以省略副本,您也不能按值传递它的实例。在这种情况下,您应该使用右值引用声明一个真正的移动赋值运算符(按值获取其参数的赋值运算符是 §12.8/19 中的复制赋值运算符,如果类不可复制,您将不希望这样做)。对于可复制和可移动的类,编译器应该使用复制省略或调用移动构造函数。
  • @Omni:虚函数不能在声明时显式默认(第 8.4.2/2 节和第 8.4.2/5 节中的最后一个示例)。
  • 自 C++11 通过以来,规则是否发生了变化?我相信struct C { virtual ~C() = default; }; 现在是允许的,也是最简洁的选择。来自 n3242 的禁令(“-它不应是虚拟的”)在 n3290 中不再存在,而 GCC 允许它,而之前它没有。
  • @BЈовић 不,这不是错字。这是一个很好的解释:stackoverflow.com/a/12306344/1174378
【解决方案2】:

我不敢相信没有人链接到this

文章基本上主张“零规则”。 我不宜引用整篇文章,但我认为这是重点:

具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权。 其他类不应该有自定义析构函数,复制/移动 构造函数或复制/移动赋值运算符。

这一点也很重要:

标准中包含常见的“包中所有权”类 库:std::unique_ptrstd::shared_ptr。通过使用 自定义删除器对象,两者都已变得足够灵活,可以管理 几乎任何类型的资源。

【讨论】:

  • 请参阅 herehere 了解我对这件事的看法。 :)
【解决方案3】:

我不这么认为,the rule of three 是一个经验法则,它指出实现以下之一但不是全部的类可能是错误的。

  1. 复制构造函数
  2. 赋值运算符
  3. 析构函数

但是,省略移动构造函数或移动赋值运算符并不意味着存在错误。 可能错失优化机会(在大多数情况下),或者移动语义与此类无关,但这不是错误。

虽然在相关时定义移动构造函数可能是最佳实践,但这不是强制性的。在许多情况下,移动构造函数与类无关(例如std::complex),并且所有在 C++03 中行为正确的类将继续在 C++0x 中正确行为,即使它们没有定义移动构造函数。

【讨论】:

    【解决方案4】:

    是的,我认为为此类类提供移动构造函数会很好,但请记住:

    • 这只是一个优化。

      仅实现复制构造函数、赋值运算符或析构函数中的一两个可能会导致错误,而没有移动构造函数则可能会降低性能。

    • Move 构造函数不能总是在不修改的情况下应用。

      有些类总是分配了它们的指针,因此这些类总是在析构函数中删除它们的指针。在这些情况下,您需要添加额外的检查来说明它们的指针是否已分配或已被移走(现在为空)。

    【讨论】:

    • 这不仅仅是优化,移动语义在完美转发中很重要,有些类(unique_ptr)没有移动语义就无法实现。
    • @DeadMG:一般来说你是对的,但在这种情况下,移动语义只是一种优化。在这里,我说的是已经存在的尊重三规则的类; unique_ptr和完美转发是一些特殊情况...
    • @peoro:这就像建议 C++ 只在 C 中添加类。auto_ptr 尊重三规则,unique_ptr 肯定不遵守五规则。
    • @peoro:我认为可以调用声明私有复制构造函数和复制赋值运算符(或从boost::noncopyable 继承)的 C++03 类来遵守三规则。 (否则我们必须引入不同的术语,例如“大一和小二的规则”)。
    • some classes have always their pointers allocated... 在这种情况下,移动通常作为交换实现。一样简单快速。 (实际上更快,因为它将释放移动到右值的析构函数)
    【解决方案5】:

    以下是自 2011 年 1 月 24 日以来的当前状态和相关发展的简短更新。

    根据 C++11 标准(参见附件 D 的 [depr.impldec]):

    如果类具有用户声明的复制赋值运算符或用户声明的析构函数,则不推荐使用复制构造函数的隐式声明。如果类具有用户声明的复制构造函数或用户声明的析构函数,则不推荐使用复制赋值运算符的隐式声明。

    实际上是proposed 废弃了已弃用的行为赋予 C++14 真正的“五规则”而不是传统的“三规则”。 2013 年,EWG 投票反对这一点建议在 C++2014 中实现。对该提案做出决定的主要理由与对破坏现有代码的普遍担忧有关。

    最近又是proposed对C++11的措辞进行了改编,以达到非正式的五法则,即

    如果这些函数中的任何一个是用户提供的,则编译器不会生成任何复制函数、移动函数或析构函数。

    如果得到 EWG 的批准,“规则”很可能会被 C++17 采用。

    【讨论】:

    • 感谢您的更新。随着其中一些 C++ 问题变得陈旧,了解问题和/或答案如何受新语言版本的影响会很有帮助。
    【解决方案6】:

    基本上是这样的:如果你不声明任何移动操作,你应该尊重三法则。如果您声明一个移动操作,那么“违反”三规则并没有什么坏处,因为编译器生成的操作的生成已经变得非常严格。即使您没有声明移动操作并违反了三规则,C++0x 编译器也会在用户声明一个特殊函数并且由于现已弃用“C++03 兼容性规则”。

    我认为可以肯定地说这条规则变得不那么重要了。 C++03 中的真正问题是,实现不同的复制语义需要您用户声明 all 相关的特殊函数,以便它们都不是编译器生成的(否则会做错事)。但是 C++0x 改变了特殊成员函数生成的规则。如果用户仅声明其中一个函数来更改复制语义,它将阻止编译器自动生成剩余的特殊函数。这很好,因为缺少声明现在会将运行时错误变成编译错误(或至少是警告)。作为 C++03 兼容性措施,仍然会生成一些操作,但这一代已被视为已弃用,并且至少应在 C++0x 模式下产生警告。

    由于编译器生成的特殊函数的限制性规则和 C++03 兼容性,三规则仍然是三规则。

    以下是一些适用于最新 C++0x 规则的示例:

    template<class T>
    class unique_ptr
    {
       T* ptr;
    public:
       explicit unique_ptr(T* p=0) : ptr(p) {}
       ~unique_ptr();
       unique_ptr(unique_ptr&&);
       unique_ptr& operator=(unique_ptr&&);
    };
    

    在上面的示例中,不需要将任何其他特殊功能声明为已删除。由于限制性规则,它们根本不会生成。用户声明的移动操作的存在会禁用编译器生成的复制操作。但在这样的情况下:

    template<class T>
    class scoped_ptr
    {
       T* ptr;
    public:
       explicit scoped_ptr(T* p=0) : ptr(p) {}
       ~scoped_ptr();
    };
    

    现在预计 C++0x 编译器会针对可能由编译器生成的复制操作发出警告,这些操作可能会做错事。在这里,三件事的规则应该得到尊重。在这种情况下发出警告是完全合适的,它让用户有机会处理错误。我们可以通过删除函数来解决这个问题:

    template<class T>
    class scoped_ptr
    {
       T* ptr;
    public:
       explicit scoped_ptr(T* p=0) : ptr(p) {}
       ~scoped_ptr();
       scoped_ptr(scoped_ptr const&) = delete;
       scoped_ptr& operator=(scoped_ptr const&) = delete;
    };
    

    因此,仅仅因为 C++03 的兼容性,三规则在这里仍然适用。

    【讨论】:

    • 事实上,N3126 确实将unique_ptr 的复制构造函数和复制赋值操作符定义为已删除——有人知道为什么吗?
    • @Philipp:限制性规则比 N3126 更新。但是,N3225 仍然将 unique_ptr 的复制操作声明为已删除。这不再是必要的,但也没有错。因此,没有必要更改 unique_ptr 的规范。
    • N3126 的规则不太严格,如果存在用户声明的移动构造函数,则不会隐式声明复制构造函数,如果存在用户声明,则不会隐式声明复制赋值运算符-声明的移动赋值运算符。 unique_ptr 有用户声明的移动构造函数和移动赋值运算符,所以我认为即使应用 N3126 规则也不需要用户声明的复制构造函数和复制赋值运算符。不是很重要,但是因为标准库类使用的约定可能被解释为最好的
    • 实践,很高兴知道显式声明的复制构造函数和复制赋值运算符是否是故意的。
    【解决方案7】:

    如果不破坏所有执行 3 规则且不实现任何形式的移动语义的现有代码,我们不能说 3 规则现在变成 4 规则(或 5 规则)。

    3 规则意味着如果您实施一个,则必须实施所有 3 个。

    也不知道会有任何自动生成的动作。 “规则 3”的目的是因为它们自动存在,如果你实现一个,很可能其他两个的默认实现是错误的。

    【讨论】:

      【解决方案8】:

      在一般情况下,是的,三的规则只是变成了五的规则,并添加了移动赋值运算符和移动构造函数。但是,并非所有类都是可复制和可移动的,有些只是可移动的,有些只是可复制的。

      【讨论】:

      • 我相信即使一个类是不可复制的,你也想定义复制构造函数和赋值运算符(已删除)。所以一个可移动的资源管理类也应该定义所有五个。
      • @Philipp,我非常不同意,许多类不支持移动语义,仅仅为了某种美学意义而定义两个冗余函数是没有意义的。为什么std::complex 应该关心右值引用?
      • @Motti:为什么要定义常规复制语义?几乎所有可以复制的资源都可以移动。
      • @Motti:Philipp 说他们应该被定义为删除!因此,您应该明确反对他们不支持该操作的事实。
      • @Konrad 这对我来说似乎过于冗长,一旦定义了 cctor,mctor 将不会被定义(据我了解当前草案)。您是否还会为定义自定义构造函数的每个类定义默认构造函数?
      【解决方案9】:

      简单来说,记住这一点。

      0 规则

      Classes have neither custom destructors, copy/move constructors or copy/move assignment operators.

      3 规则: 如果您实现其中任何一个的自定义版本,您就实现了所有这些。

      Destructor, Copy constructor, copy assignment

      5 规则: 如果您实现自定义移动构造函数或移动赋值运算符,则需要定义所有 5 个。需要移动语义。

      Destructor, Copy constructor, copy assignment, move constructor, move assignment

      四个半规则: 与规则 5 相同,但具有复制和交换习语。通过包含 swap 方法,复制赋值和移动赋值合并为一个赋值运算符。

      Destructor, Copy constructor, move constructor, assignment, swap (the half part)

      参考文献

      https://www.linkedin.com/learning/c-plus-plus-advanced-topics/rule-of-five?u=67551194 https://en.cppreference.com/w/cpp/language/rule_of_three

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-11
        • 2011-04-11
        • 2012-08-30
        • 1970-01-01
        相关资源
        最近更新 更多