三阶规则与C ++ 11一起成为五次规则?

时间:2011-01-24 13:51:46

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

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

9 个答案:

答案 0 :(得分:308)

我会说三法则成为三,四和五的规则:

  

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

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

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

     

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

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

请注意,不会为显式声明任何其他特殊成员函数的类生成移动构造函数和移动赋值运算符,也不会为显式声明移动的类生成复制构造函数和复制赋值运算符构造函数或移动赋值运算符,以及具有显式声明的析构函数和隐式定义的复制构造函数或隐式定义的复制赋值运算符的类被视为已弃用。特别是,以下完全有效的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中那样。如果你有一个移动构造函数和移动赋值运算符,那么专门化std::swap将变得不重要,因为泛型std::swap使用移动构造函数并移动赋值运算符(如果可用),并且应该足够快。

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

答案 1 :(得分:65)

我无法相信没有人与this相关联。

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

  

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

这一点也很重要:

  

标准中包含常见的“一揽子所有权”类   图书馆:std::unique_ptrstd::shared_ptr。通过使用   自定义删除对象,都具有足够的灵活性来管理   几乎任何类型的资源。

答案 2 :(得分:18)

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

  1. 复制构造函数
  2. 作业运算符
  3. 析构
  4. 但是,省略移动构造函数或移动赋值运算符并不意味着错误。 可能错过了优化的机会(在大多数情况下),或者移动语义与此类无关,但这不是错误。

    虽然最佳做法是在相关时定义移动构造函数,但这不是强制性的。在很多情况下,移动构造函数与类无关(例如std::complex),所有在C ++ 03中行为正确的类将继续在C ++ 0x中正常运行,即使它们不是定义一个移动构造函数。

答案 3 :(得分:14)

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

  • 这只是一次优化。

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

  • 无法在不进行修改的情况下始终应用移动构造函数。

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

答案 4 :(得分:8)

以下是自1月24日至11日以来当前状况和相关发展的简短更新。

根据C ++ 11标准(见附件D&#39; s [depr.impldec]):

  

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

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

最近,proposed再次调整了C ++ 11的措辞,以实现非正式的五法则,即

  

如果用户提供任何这些功能,则不会编译生成复制函数,移动函数或析构函数。

如果得到EWG的批准,&#34;规则&#34;可能会被C ++ 17采用。

答案 5 :(得分:4)

基本上,就像这样:如果你没有宣布任何移动操作,你应该尊重三个规则。如果你声明一个移动操作,那么“违反”三个规则就没有坏处,因为编译器生成的操作的生成受到了非常严格的限制。即使您没有声明移动操作并违反三规则,C ++ 0x编译器也会在用户声明一个特殊函数并且由于某个特殊函数被自动生成的情况下给您一个警告。现已弃用“C ++ 03兼容性规则”。

我认为可以说这条规则变得不那么重要了。 C ++ 03中的真正问题是实现不同的复制语义要求用户声明所有相关的特殊函数,以便它们都不是编译器生成的(否则会做错误的事情)。但是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兼容性。

答案 6 :(得分:3)

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

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

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

答案 7 :(得分:2)

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

答案 8 :(得分:0)

简单来说,记住这一点。

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 相同,但具有复制和交换习语。通过包含交换方法,复制赋值和移动赋值合并为一个赋值运算符。

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