为啥析构函数会禁用隐式移动方法的生成?

Posted

技术标签:

【中文标题】为啥析构函数会禁用隐式移动方法的生成?【英文标题】:Why does destructor disable generation of implicit move methods?为什么析构函数会禁用隐式移动方法的生成? 【发布时间】:2016-02-29 04:56:50 【问题描述】:

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

Example:

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

"添加析构函数有禁用的副作用 生成移动功能,但因为 Widget 是可复制的,所以所有 用于生成移动的代码现在将生成副本。在 换句话说,向类添加析构函数会导致 大概有效的动作被悄悄地替换为 可能是效率较低的副本”。

Scott Meyers 的上述文字,在我的脑海中引发了一些问题:

为什么声明析构函数会隐藏移动语义? 是否声明/定义析构函数只隐藏移动语义或复制 构造函数和复制赋值也隐藏了移动语义?

【问题讨论】:

这条规则背后的基本原理是,如果你的代码有自定义析构函数,可能是因为需要释放资源;如果需要释放资源,则 move 的默认实现可能会使源对象处于虚假状态。 不清楚什么是“声明/定义析构函数只隐藏移动语义或复制构造函数和复制赋值以及隐藏移动语义?”在问。 问题中提到的链接不起作用。 【参考方案1】:

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

我的班级是否管理资源?

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

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

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

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

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

有关详细信息,请参阅:

    https://akrzemi1.wordpress.com/2015/09/08/special-member-functions/ https://akrzemi1.wordpress.com/2015/09/11/declaring-the-move-constructor/

【讨论】:

【参考方案2】:

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

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

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

【讨论】:

嗨皮特非常感谢您的回复,根据您的说法,如果我们明确定义 Dtor(安全方法),编译器将隐藏隐式移动......出于安全目的编译器会隐藏...与移动相比,我读到复制操作成本很高...尽管我们明确定义了移动,但隐藏是否发生(我不这么认为)。 所以,一般的想法是“如果没有析构函数,移动某物是安全的”。析构函数意味着销毁对象需要一些特殊的操作(删除某些内容,将引用倒计时等)。在移动语义中,这是通过将对象从一侧“交换”到另一侧来完成的,而不是制作副本。当然,编译器不可能真正知道它如何与“你需要在析构函数中做的任何事情”一起玩(除非它理解析构函数的作用是什么,并且在不可能知道之前不需要太多时间) !)【参考方案3】:

正在声明/定义 Dtor 仅隐藏移动语义或复制 ctor/copy 赋值以及隐藏移动语义?

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

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

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

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

【讨论】:

我认为 OP 正在询问这些规则背后的理由。 非常感谢 101010,隐藏发生的依据是 R0/R3/R5 之类的规则还是取决于编译器。 不,这些是所有编译器都遵循的标准规则。【参考方案4】:

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

第二,作为补充,我想详细说明一下。

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

来自c++draft:

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

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

来自cppreference:

如果没有为类提供用户定义的移动构造函数,则所有 以下是正确的:

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

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

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

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

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

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

如果有用户声明的析构函数,这意味着有一些清理工作要做,那么您可能希望使用 moved-from 对象来完成。

当存在用户声明的移动赋值运算符时,由于它也是“移动”资源,您可能希望在移动 ctor 中执行相同操作。

当存在用户声明的复制构造函数或复制赋值运算符时,这是最有趣的情况。我们知道move semantics allows us to keep value semantics while gaining performance optimization,当没有提供移动ctor时,移动将“退回”复制。在某种程度上,移动可以被视为“优化副本”。因此,如果复制操作需要我们做某事,很可能在移动操作中也需要做类似的工作。

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

【讨论】:

以上是关于为啥析构函数会禁用隐式移动方法的生成?的主要内容,如果未能解决你的问题,请参考以下文章

C++析构拷贝赋值移动拷贝函数的几个知识点(不全)

5 规则(用于构造函数和析构函数)过时了吗?

C++11 虚拟析构函数和移动特殊函数的自动生成

为啥C++里面,析构函数会被调用两次

为啥纯虚析构函数需要实现

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数