三法则变成五法则与C++11?[已结束]

Rule-of-Three becomes Rule-of-Five with C++11? [closed]

提问人:Xeo 提问时间:1/24/2011 最后编辑:FlexoXeo 更新时间:11/23/2021 访问量:78913

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章来用事实和引文来回答。

去年关闭。

社区去年审查了是否重新讨论这个问题,并关闭了它:

原始关闭原因未解决

因此,在观看了这个关于右值引用的精彩讲座后,我认为每个类都会受益于这样的“移动构造函数”、编辑,当然还有“移动赋值运算符”,正如 Philipp 在他的回答中指出的那样,如果它有动态分配的成员,或者通常存储指针。就像你应该有一个复制者、赋值运算符和析构函数一样,如果前面提到的几点适用的话。 思潮?template<class T> MyClass(T&& other)template<class T> MyClass& operator=(T&& other)

C++ 构造函数 C++11 rvalue-reference 三法则

评论

0赞 cpprust 3/31/2023
谢谢你的建议!Stephan T. Lavavej 的教程非常有帮助!

答:

14赞 peoro 1/24/2011 #1

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

  • 这只是一种优化。

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

  • 如果不进行修改,则不能始终应用 Move 构造函数。

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

评论

21赞 Puppy 1/24/2011
这不仅仅是一种优化,移动语义在完美转发中很重要,如果没有移动语义,某些类()就无法实现。unique_ptr
0赞 peoro 1/24/2011
@DeadMG:总的来说,你是对的,但在这种情况下,移动语义只是一种优化。在这里,我谈论的是已经存在的类,它们尊重三法则; 和完美转发是一些特例......unique_ptr
0赞 Puppy 1/24/2011
@peoro:这就像建议C++只给C语言添加类。 尊重三法则,当然也不尊重五法则。auto_ptrunique_ptr
1赞 Philipp 1/24/2011
@peoro:我认为可以调用声明私有副本构造函数和复制赋值运算符(或继承自 )的 C++03 类来遵守三法则。(否则,我们必须引入不同的术语,例如“大一和小二的规则”)。boost::noncopyable
4赞 Mooing Duck 8/27/2011
some classes have always their pointers allocated...在这种情况下,移动通常以交换的形式实现。同样简单快捷。(实际上更快,因为它将释放移动到右值的析构函数)
3赞 CashCow 1/24/2011 #2

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

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

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

336赞 Philipp 1/24/2011 #3

我会说三法则变成了三、四和五法则:

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

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

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

通常,以下一组特殊成员之一 函数是明智的:

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

注意:

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

特别是,以下完全有效的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规则的类仍然可以。

对于资源管理类,如果无法复制基础资源,则可以将复制构造函数和复制赋值运算符定义为已删除(计为定义)。通常,您仍然需要移动构造函数和移动赋值运算符。复制和移动赋值运算符通常使用 实现,如在 C++03 中。谈论;如果我们已经有了 move-constructor 和 move-assignment 运算符,那么专门化 std::swap 将变得不重要,因为泛型使用 move-constructor 和 move-assignment 运算符(如果可用)(这应该足够快)。swapswapstd::swap

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

评论

1赞 Xeo 1/24/2011
@Philipp:嗯,对......如果您只是实现 move-ctor,则按值传递赋值运算符“other”将被移动构造,如果我做对了?我认为其余的指针副本和赋值将由编译器优化......
2赞 Philipp 1/24/2011
@Xeo:我相信,如果类是不可复制的,那么即使可以省略复制,你也不能按值传递它的实例。在这种情况下,您应该使用右值引用声明一个真正的移动赋值运算符(按值获取其参数的赋值运算符是 §12.8/19 的复制赋值运算符,如果类不可复制,则不需要该运算符)。对于可复制类和可移动类,编译器应使用复制省略或调用移动构造函数。
1赞 Philipp 2/9/2011
@Omni:虚函数不能在声明时显式默认(§8.4.2/2 和 §8.4.2/5 中的最后一个示例)。
10赞 Luc Danton 9/22/2011
自从 C++11 通过以来,规则是否发生了变化?我相信现在是允许的,也是最简洁的选择。n3242 中的禁令(“- 它不应是虚拟的”)在 n3290 中不再存在,GCC 允许它,而以前它不允许。struct C { virtual ~C() = default; };
3赞 Mihai Todor 9/7/2012
@B不,这不是错别字。这里有一个很好的解释:stackoverflow.com/a/12306344/1174378
2赞 Puppy 1/24/2011 #4

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

评论

1赞 Philipp 1/24/2011
我相信即使一个类是不可复制的,你也想定义复制构造函数和赋值运算符(已删除)。因此,一个可移动的资源管理类也应该定义所有五个。
1赞 Motti 1/24/2011
@Philipp,我强烈不同意,许多类不支持移动语义,仅仅为了某种美感而定义两个冗余函数是没有意义的。为什么要关注右值引用?std::complex
0赞 Puppy 1/24/2011
@Motti:为什么它定义了常规的复制语义?几乎所有可以复制的资源都可以移动。
0赞 Konrad Rudolph 1/24/2011
@Motti:菲利普说他们应该被定义为已删除!因此,您应该明确反对他们不支持该操作的事实。
0赞 Motti 1/24/2011
@Konrad这对我来说似乎过于冗长,但一旦定义了 cctor,mctor 就不会被定义(据我所知,当前的草案)。您是否还会将默认构造函数定义为为定义自定义构造函数的每个类删除?
21赞 Motti 1/24/2011 #5

我不这么认为,三法则是一种经验法则,它指出实现以下其中一项但不是全部的类可能是有问题的。

  1. Copy 构造函数
  2. 赋值运算符
  3. 破坏者

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

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

4赞 sellibitze 1/25/2011 #6

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

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

由于关于编译器生成的特殊函数和 C++03 兼容性的规则相当严格,因此三法则仍然是三法则。

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

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++0 编译器应该会生成一个警告,说明编译器生成的复制操作可能会做错事。在这里,三事规则应该得到尊重。在这种情况下,警告是完全合适的,并让用户有机会处理错误。我们可以通过删除的功能来摆脱这个问题:

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 兼容性。

评论

0赞 Philipp 1/25/2011
事实上,N3126 确实将 copy 构造函数和复制赋值运算符定义为已删除 — 有人知道为什么吗?unique_ptr
0赞 sellibitze 1/25/2011
@Philipp:限制性规则比 N3126 更新。但是,N3225 仍将 unique_ptr 的复制操作声明为已删除。这已经没有必要了,但也没有错。因此,没有必要更改unique_ptr的规格。
0赞 Philipp 1/25/2011
N3126 的规则不太严格,如果存在用户声明的移动构造函数,则不会隐式声明复制构造函数,如果存在用户声明的移动赋值运算符,则不会隐式声明复制赋值运算符。 同时具有用户声明的移动构造函数和移动赋值运算符,因此我认为即使在应用 N3126 规则时,用户声明的复制构造函数和复制赋值运算符也不需要。这并不重要,但因为标准库类使用的约定可能会被解释为最佳unique_ptr
0赞 Philipp 1/25/2011
在实践中,很高兴知道显式声明的复制构造函数和复制赋值运算符是否是有意的。
73赞 NoSenseEtAl 12/19/2012 #7

我不敢相信没有人与有关。

基本上,文章主张“零规则”。 我引用整篇文章是不合适的,但我相信这是重点:

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

恕我直言,这一点也很重要:

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

评论

4赞 Xeo 12/19/2012
请看 这里这里 了解我对整个事情的看法。:)
8赞 Andrey Rekalo 1/12/2015 #8

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

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

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

实际上,有人提议取消已弃用的行为,使C++14成为真正的“五法则”,而不是传统的“三法则”。2013年,EWG投票反对这项将在C++ 2014中实施的提案。对该提案做出决定的主要理由与对破坏现有代码的普遍担忧有关。

最近,再次有人提议调整C++11的措辞,以实现非正式的五条规则,即

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

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

评论

1赞 cb4 4/2/2016
感谢您的更新。由于其中一些 C++ 问题变得陈旧,因此了解问题和/或答案如何受到较新语言版本的影响会很有帮助。
0赞 zeitgeist 6/21/2021 #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 法则相同,但带有复制和交换成语。加入交换方法后,复制分配和移动分配将合并到一个分配运算符中。

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