【问题标题】:Why do we need default generated destructor为什么我们需要默认生成的析构函数
【发布时间】:2020-11-08 11:19:08
【问题描述】:

什么情况下我们需要默认生成析构函数?很清楚为什么我们需要默认生成的构造函数和 operator=,但是想不出应该使用默认生成的析构函数的情况。

class A
{
...
~A() = default;
...
};

【问题讨论】:

  • 我很确定~A() = default; 总是等价于~A() {},所以提供的语法主要是为了对称;如果在某些地方允许=default 而在其他地方不允许,那就太奇怪了。至于为什么您可能需要显式编写 ~A() {} 而不是完全省略析构函数 - 例如当你想做它virtual
  • @IgorTandetnik, gcc.godbolt.org/z/eh3Waz
  • 一个空的析构函数将平凡的类型变成非平凡的类型。它可能会导致某些模板代码采用非最佳路径。
  • @DoehJohn 我做了一个例子。

标签: c++ c++11 destructor


【解决方案1】:

C++ Core Guidelines C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all

原因

复制、移动和销毁的语义密切相关,所以如果需要声明一个,那么其他的可能也需要考虑。

声明任何复制/移动/析构函数,即使是 =default 或 =delete,都将禁止隐式声明移动构造函数和移动赋值运算符。声明移动构造函数或移动赋值运算符,即使是 =default 或 =delete,也会导致隐式生成的复制构造函数或隐式生成的复制赋值运算符被定义为已删除。因此,一旦声明了其中任何一个,其他的都应该声明以避免不必要的影响,例如将所有潜在的移动变成更昂贵的副本,或者使一个类只移动。

注意 如果您想要一个默认实现(同时定义另一个),请编写 =default 以表明您是故意为该功能这样做的。如果您不想要生成的默认函数,请使用 =delete 取消它。

所以这主要取决于类中声明的内容。

一般是The rule of three/five/zero

如果类需要自定义复制/移动功能,但对于析构函数没有什么特别之处,则应在析构函数上使用=default

【讨论】:

  • 但是如果程序员不自己写析构函数,默认的dtor总是会生成
  • @DoehJohn 你是对的——它将被创建。核心指南有以下“注意:如果您想要一个默认实现(同时定义另一个),请写 =default 以表明您是故意为该功能这样做的。”
【解决方案2】:

如果您想在内部类中隐藏类的实现并将unique_ptr 保留到该内部类的实例(pimpl 成语),您需要移动default 析构函数类定义之外的定义,因为unique_ptr 不能用于不完整的类型。

例子:

A.hpp(该类的用户将包含的标题)

#pragma once
#include <memory>

class A {
public:
    A();
    ~A();
    void foo() const;
private:
    struct A_impl; // just forward declared
    std::unique_ptr<A_impl> pimpl;
};

A_impl.hpp(“隐藏”- 不包含在A 的正常使用中)

#pragma once
#include "A.hpp"

struct A::A_impl {
    void foo() const;
};

A.cpp

#include "A_impl.hpp"

A::A() : pimpl(std::make_unique<A_impl>()) {}
A::~A() = default;                            // <- moved to after A_impl is fully defined
void A::foo() const { pimpl->foo(); }

A_impl.cpp

#include "A_impl.hpp"

#include <iostream>

void A::A_impl::foo() const { std::cout << "foo\n"; }

Demo

如果让编译器生成A::~A(),它将无法编译。我的编译器说:

unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘A::A_impl’
     static_assert(sizeof(_Tp)>0,
                   ^~~~~~~~~~~

Demo

【讨论】:

  • 如果你将你的类声明为 class A { public: A();无效 foo() 常量;私人:结构A_impl; // 只转发声明的 std::unique_ptr pimpl; };你有这个错误吗?
  • @DoehJohn 是的,dtor 必须在 A_impl 完全定义后定义,所以这是 ~A() = default; 非常有意义的情况。我添加了两个演示,以便您可以现场观看。
  • 非常感谢您的宝贵时间!给A添加析构函数解决了'A::A_impl'类型不完整的错误有点令人困惑
  • @DoehJohn 不客气。是的,我想一开始可能会有点混乱。 This 看起来不错。
【解决方案3】:

这似乎是在询问您何时为一个类定义析构函数,如果该析构函数的主体与编译器生成的主体相同。

原因包括:

  1. 清晰。如果您有一个带有复制/移动构造函数或复制/移动赋值运算符的类,它通常管理一些资源。许多编码指南要求您定义析构函数以表明它不仅被忽略,甚至等同于编译器生成的析构函数。
  2. 函数的某些方面与编译器生成的不同。如果你想要一个虚拟析构函数,你必须定义它。同样,必须定义一个抛出析构函数。
  3. 您想控制析构函数的生成位置。您可以在类定义之外定义析构函数。您可能需要像其他答案之一那样对循环依赖的类执行此操作。您可能希望这样做来定义一个稳定的 ABI。您可能希望这样做来控制代码生成。

在所有这些情况下,您必须或想要定义析构函数,即使主体没有什么特别之处。为什么你会使用= default 而不是一个空的身体?因为编译器生成的析构函数等同于您使用= default 获得的析构函数,并且您只想更改析构函数的各个方面试图 更改。空主体与 C++ 中的 = default 不同,因为可以将默认函数定义为已删除。空体也排除了微不足道的可破坏性,即使这是一种选择。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-11-16
    • 2017-01-10
    • 2011-03-05
    • 1970-01-01
    • 1970-01-01
    • 2012-12-25
    • 1970-01-01
    相关资源
    最近更新 更多