5 法则对于构造函数和析构函数是否过时了?

问题描述

规则 5 规定,如果一个类具有用户声明的析构函数、复制构造函数、复制赋值构造函数、移动构造函数或移动赋值构造函数,那么它必须具有其他 4 个。

但今天我突然明白了:你什么时候需要用户定义的析构函数、复制构造函数、复制赋值构造函数、移动构造函数或移动赋值构造函数

在我的理解中,隐式构造函数/析构函数对于聚合数据结构来说工作得很好。但是,管理资源的类需要用户定义的构造函数/析构函数

但是,不能使用智能指针将所有资源管理类转换为聚合数据结构吗?

示例:

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

对比

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

现在示例 2 的行为与示例 1 完全相同,但所有隐式构造函数都可以工作。

当然,您不能复制 ResourceManager,但如果您想要不同的行为,您可以使用不同的智能指针。

关键是当智能指针已经有那些隐式构造函数可以工作时,你不需要用户定义的构造函数

我认为拥有用户定义构造函数的唯一原因是:

  1. 你不能在一些低级代码中使用智能指针(我非常怀疑这种情况)。

  2. 您正在自己实现智能指针。

但是,在普通代码中,我看不出有任何理由使用用户定义的构造函数

在这里遗漏了什么吗?

解决方法

规则的全名是the rule of 3/5/0

没有说“总是提供所有五个”。它表示您必须要么提供三个、五个或一个都不提供。

事实上,最明智的做法往往是不提供这五个中的任何一个。但是,如果您正在编写自己的容器、智能指针或围绕某些资源的 RAII 包装器,则无法做到这一点。

,

但是,在普通代码中,我看不出有任何理由使用用户定义的构造函数。

用户提供的构造函数也允许保持一些不变性,因此与规则 5 正交。

例如一个

struct clampInt
{
    int min;
    int max;
    int value;
};

不确保 min < max。所以封装数据可能提供这种保证。 聚合并不适合所有情况。

您什么时候需要用户定义的析构函数、复制构造函数、复制赋值构造函数、移动构造函数或移动赋值构造函数?

现在关于 5/3/0 规则。

确实应该首选 0 规则。

可用的智能指针(我包括容器)用于指针、集合或 Lockables。 但是资源不是必需的指针(可能是隐藏在 int 中的句柄、内部隐藏的静态变量 (XXX_Init()/XXX_Close())),或者可能需要更高级的处理(对于数据库,在范围结束时自动提交或在异常情况下回滚),因此您必须编写自己的 RAII 对象。

您可能还想编写并不真正拥有资源的 RAII 对象,例如作为 TimerLogger(写入“范围”使用的经过时间)。

另一个通常必须为抽象类编写析构函数的时刻,因为您需要虚拟析构函数(并且可能的多态复制由虚拟 clone 完成)。

,

拥有已经遵循五法则的良好封装概念确实确保您不必担心它。也就是说,如果您发现自己处于必须编写一些自定义逻辑的情况,它仍然成立。想到的一些事情:

  • 您自己的智能指针类型
  • 必须注销的观察者
  • C 库的包装器

接下来,我发现一旦你有足够的组合,就不再清楚类的行为将是什么。赋值运算符可用吗?我们可以复制构造类吗?因此,强制执行 5 规则,即使其中包含 = default,并结合 -Wdefaulted-function-deleted 作为错误有助于理解代码。

仔细查看您的示例:

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

这段代码确实可以很好地转换为:

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

但是,现在想象一下:

class ResourceManager {
    ResourcePool &pool;
    Resource *resource;

    ResourceManager(ResourcePool &pool) : pool{pool},resource{pool.createResource()} {}
    ~ResourceManager() { pool.destroyResource(resource);
};

同样,如果你给它一个自定义析构函数,这可以用 unique_ptr 来完成。 但是,如果您的类现在存储了大量资源,您是否愿意支付额外的内存成本?

如果在将资源返回到池中进行回收之前首先需要锁定怎么办?你会只拿这个锁一次并返回所有资源还是 1000 次 1 对 1 返回?

我认为您的推理是正确的,拥有良好的智能指针类型会降低 5 规则的相关性。但是,正如本答案中所指出的,总有一些情况需要您去发现。所以说它过时可能有点过时,有点像知道如何使用 for (auto it = v.begin(); it != v.end(); ++it) 而不是 for (auto e : v) 进行迭代。您不再使用第一个变体,到目前为止,您需要调用“擦除”,而这突然再次变得相关。

,

该规则经常被误解,因为它经常被发现过于简单化。

简化版本是这样的:如果您需要编写至少一个(3/5)特殊方法,那么您需要编写所有(3/5)。

实际有用规则:负责手动拥有资源的类应该: 专门处理资源的所有权/生命周期;为了正确地做到这一点,它必须实现所有 3/5 特殊成员。否则(如果您的班级没有资源的手动所有权),您必须将所有特殊成员保留为隐式或默认值(零规则)。

简化版本使用这种修辞:如果您发现自己需要编写 (3/5) 中的一个,那么很可能您的类手动管理资源的所有权,因此您需要实现所有 (3/5)。

示例 1:如果您的类管理系统资源的获取/释放,则它必须实现所有 3/5。

示例 2:如果您的类管理内存区域的生命周期,那么它必须实现所有 3/5。

示例 3: 在您的析构函数中进行一些日志记录。您编写析构函数的原因不是为了管理您拥有的资源,因此您不需要编写其他特殊成员。

结论:在用户代码中,您应该遵循零规则:不要手动管理资源。使用已为您实现此功能的 RAII 包装器(例如智能指针、标准容器、std::string 等)

但是,如果您发现自己需要手动管理资源,请编写一个专门负责资源生命周期管理的 RAII 类。此类应实现所有 (3/5) 特殊成员。

这方面的好书:https://en.cppreference.com/w/cpp/language/rule_of_three

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...