如何解决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
,但如果您想要不同的行为,您可以使用不同的智能指针。
关键是当智能指针已经有那些隐式构造函数可以工作时,你不需要用户定义的构造函数。
-
你不能在一些低级代码中使用智能指针(我非常怀疑这种情况)。
-
您正在自己实现智能指针。
但是,在普通代码中,我看不出有任何理由使用用户定义的构造函数。
我在这里遗漏了什么吗?
解决方法
规则的全名是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
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。