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

Is the Rule of 5 (for constructors and destructors) outdated?

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

但今天我恍然大悟:什么时候需要用户定义的析构函数、复制构造函数、复制赋值构造函数、移动构造函数或移动赋值构造函数?

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

但是,不是所有的资源管理class都可以使用智能指针转换成聚合数据结构吗?

示例:

// 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

不会说“总是提供所有五个”。它说您必须 提供其中的三个、五个或 none。

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

However, in normal code I don't see any reason to use user-defined constructors.

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

例如a

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

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

when do you ever need a user-defined destructor, copy constructor, copy assignment constructor, move constructor, or move assignment constructor?

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

确实应该首选 0 规则。

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

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

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

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

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

除此之外,我发现一旦你有足够的组合,就不再清楚 class 的行为是什么。赋值运算符可用吗?我们可以复制构造 class 吗?因此,执行五规则,即使其中包含 = 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 来完成。 不过,如果你的class现在存储了很多资源,你愿意为内存付出额外的代价吗?

如果您需要先获取锁,然后才能return资源到池中被回收怎么办?你会只拿这个锁一次和 return 所有资源,还是当你 return 他们 1 对 1 时拿 1000 次?

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

如前所述,完整规则是 0/3/5 规则;通常执行其中的 0 个,如果你执行任何一个,则执行其中的 3 个或 5 个。

在少数情况下,您必须执行 copy/move 和销毁操作。

  1. 自参考。有时一个对象的部分引用对象的其他部分。当您复制它们时,它们会天真地引用您从中复制的 other 对象。

  2. 智能指针。有理由实施更多的智能指针。

  3. 比智能指针、资源拥有类型更普遍,如 vectors 或 optionalvariants。所有这些都是让他们的用户不关心他们的词汇类型。

  4. 比 1 更通用,对象的身份很重要。例如,具有外部注册的对象必须向注册存储重新注册新副本,并且在销毁时必须自行注销。

  5. 由于并发而不得不小心或花哨的情况。例如,如果您有一个 mutex_guarded<T> 模板并且您希望它们是可复制的,则默认复制不起作用,因为包装器具有互斥锁,并且无法复制互斥锁。在其他情况下,您可能需要保证某些操作的顺序,进行比较和设置,甚至跟踪或记录对象的“本机线程”以检测它何时跨越线程边界。

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

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

实际的、有用的规则:负责手动拥有资源的class应该:专门处理管理资源的ownership/lifetime资源;为了正确地做到这一点,它必须实现所有 3/5 特殊成员。否则(如果您的 class 没有资源的手动所有权)您必须让所有特殊成员隐式或默认(零规则)。

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

示例 1: 如果您的 class 管理系统资源的 acquisition/release,那么它必须实现全部 3/5。

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

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

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

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

很好的读物:https://en.cppreference.com/w/cpp/language/rule_of_three