21

通过阅读此博客,我试图了解零规则的含义。IMO,它说如果您声明自己的析构函数,那么不要忘记将移动构造函数和移动赋值作为默认值。

示例

class Widget {
public:
  ~Widget();         // temporary destructor
  ...                // no copy or move functions
};

“添加析构函数的副作用是禁止生成移动函数,但由于 Widget 是可复制的,所有用于生成移动的代码现在都会生成副本。也就是说,在类中添加析构函数可能会导致- 用可能效率较低的副本默默地取代高效的举措”。

Scott Meyers 的上述文字,在引号内引起了我的一些问题:

  • 为什么声明析构函数会隐藏移动语义?
  • 声明/定义析构函数是否仅隐藏移动语义或复制构造函数和复制分配以及隐藏移动语义?
4

4 回答 4

24

“零规则”实际上是关于生成什么特殊成员函数以及何时生成的其他内容。这是关于对班级设计的某种态度。它鼓励你回答一个问题:

我的班级是否管理资源?

如果是这样,则应将每个资源移至其专用类,以便您的类仅管理资源(不做任何其他事情)或仅累积其他类和/或执行相同的逻辑任务(但不管理资源)。

这是更一般的单一职责原则的一个特例。

当您应用它时,您会立即看到,对于资源管理类,您必须手动定义移动构造函数、移动赋值和析构函数(很少需要复制操作)。对于非资源类,您不需要(实际上您可能不应该)声明以下任何一项:移动 ctor/assignment、复制 ctor/assignment、析构函数。

因此名称中的“零”:当您将类与资源管理和其他分开时,在“其他”中您需要提供零特殊成员函数(它们将正确自动生成。

C++ 中有一些规则(特殊成员函数的)定义会抑制其他定义,但它们只会分散您对零规则核心的理解。

有关更多信息,请参阅:

  1. https://akrzemi1.wordpress.com/2015/09/08/special-member-functions/
  2. https://akrzemi1.wordpress.com/2015/09/11/declaring-the-move-constructor/
于 2015-11-26T07:49:45.903 回答
6

几乎总是,如果你有一个析构函数(“做某事”),你应该遵循“三规则”,如果你想要移动语义,它就会变成“五规则”。

如果您的析构函数为空,则不需要它。所以这意味着一个非空的析构函数(因为如果不需要它,你就不会有一个!),那么你也需要在复制和赋值操作中做同样的事情,并且大概,移动构造和移动赋值将需要“做某事”,而不仅仅是传递实际内容。

当然,在某些情况下可能不是这样,但编译器采用“只有在析构函数为空时才应用自动生成的移动函数”的方法,因为那是“安全”的方法。

于 2015-11-26T07:40:48.803 回答
3

声明/定义 Dtor 是否仅隐藏移动语义或复制 ctor/复制分配以及隐藏移动语义?

如果没有为类提供用户定义的移动构造函数,则以下所有情况均成立:

  • 没有用户声明的复制构造函数
  • 没有用户声明的复制赋值运算符
  • 没有用户声明的移动赋值运算符
  • 没有用户声明的析构函数

然后编译器将声明一个移动构造函数作为其类的非显式内联公共成员signature T::T(T&&)

因此,的,声明复制构造函数或赋值运算符也隐藏了隐式声明的移动构造函数。

于 2015-11-26T07:45:06.767 回答
1

首先,我想说 Mats Petersson 的答案比公认的更好,因为它提到了理由。

其次,作为补充,我想详细说明一下。

隐式声明(或默认)移动 ctor 的行为

来自c++draft

非联合类 X 的隐式定义的复制/移动构造函数执行其基类和成员的成员复制/移动。

编译器隐式声明移动 ctor 时的条件

cppreference

如果没有为类提供用户定义的移动构造函数,则以下所有情况均成立:

  • 没有用户声明的复制构造函数
  • 没有用户声明的复制赋值运算符
  • 没有用户声明的移动赋值运算符
  • 没有用户声明的析构函数

然后编译器将声明一个移动构造函数作为其类的非显式内联公共成员,并带有签名T::T(T&&)

为什么 dtor(和许多其他人)会阻止隐式声明的移动 ctor?

如果我们看上面的条件,不仅用户声明的析构函数可以防止隐式声明的移动 ctor,用户声明的复制构造函数、用户声明的复制赋值运算符和用户声明的移动赋值运算符都具有相同的预防效果。

正如 Mats Petersson 所指出的,其基本原理是:

如果编译器认为您可能需要在移动操作中执行成员移动以外的操作,那么假设您不需要它是不安全的。

  • 当有用户声明的析构函数时,这意味着需要进行一些清理工作,那么您可能希望对移出的对象进行处理。

  • 当有用户声明的移动赋值运算符时,因为它也是“移动”资源,你可能希望在移动 ctor 中做同样的事情。

  • 当存在用户声明的复制构造函数或复制赋值运算符时,这是最有趣的情况。我们知道,移动语义允许我们在获得性能优化的同时保持值语义,并且当未提供移动 ctor 时,移动将“回退”到复制。在某种程度上,移动可以被视为“优化副本”。因此,如果复制操作需要我们做一些事情,很可能在移动操作中也需要做类似的工作。

由于在上述条件下,可能需要执行成员移动以外的操作,编译器不会假定您不需要它,因此不会隐式声明移动 ctor。

于 2018-05-23T14:08:35.787 回答