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
};
// R
class ResourceManager {
std::unique_ptr<Resource> resource;
};
class ResourceManager {
std::unique_ptr<Resource> resource;
};
AII 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
};
对比
现在示例 2 的行为与示例 1 完全相同,但所有隐式构造函数都可以工作。
当然,你不能 copy ResourceManager
,但如果你想要不同的行为,你可以使用不同的智能指针。
关键是当智能指针已经有那些隐式构造函数可以工作时,你不需要用户定义的构造函数。
我认为拥有用户定义的构造函数的唯一原因是:
- 你不能在一些低级代码中使用智能指针(我非常怀疑这种情况)。
- 您正在自己实现智能指针。
但是,在普通代码中,我看不出有任何理由使用用户定义的构造函数。
我在这里错过了什么吗?
回答
规则的全称是3/5/0 的规则。
它没有说“总是提供所有五个”。它说,你必须要么提供三,五,或没有。
事实上,最明智的做法往往是不提供这五个中的任何一个。但是,如果您正在编写自己的容器、智能指针或围绕某些资源的 RAII 包装器,则不能这样做。
- Let's say you need to have a pointer that points to a member. If you copy the object, you need to update this pointer. Thus, you need a custom (or deleted) copy constructor and assignment operator. You don't need a destructor.
- @Mark This requires `=delete`ing copy operations, which IMO counts as providing them for the purposes of the rule of 3.
- Event this version of the rule isn't one that should *always* be followed. There are exceptions.
回答
但是,在普通代码中,我看不出有任何理由使用用户定义的构造函数。
用户提供的构造函数还允许保持一些不变性,因此与规则 5 正交。
例如一个
struct clampInt
{
int min;
int max;
int value;
};
不保证min < max
。所以封装数据可能提供这种保证。聚合并不适合所有情况。
什么时候需要用户定义的析构函数、复制构造函数、复制赋值构造函数、移动构造函数或移动赋值构造函数?
现在关于 5/3/0 的规则。
确实应该首选 0 规则。
可用的智能指针(我包括容器)用于指针、集合或Lockables。但是资源不是必需的指针(可能是隐藏在内部隐藏静态变量(/ )中的句柄),或者可能需要更高级的处理(对于数据库,在范围结束时自动提交或在异常情况下回滚)所以你必须编写自己的 RAII 对象。int
XXX_Init()
XXX_Close()
您可能还想编写并不真正拥有资源的 RAII 对象,TimerLogger
例如(写入“范围”使用的经过时间)。
您通常必须为抽象类编写析构函数的另一个时刻,因为您需要虚拟析构函数(并且可能的多态复制由 virtual 完成clone
)。
回答
如前所述,完整规则是 0/3/5 规则;通常实施其中的 0 个,如果您实施任何一个,则实施其中的 3 或 5 个。
在少数情况下,您必须实现复制/移动和销毁操作。
- 自参考。有时一个对象的部分引用对象的其他部分。当您复制它们时,它们会天真地引用您从中复制的另一个对象。
- 智能指针。有理由实现更多的智能指针。
- 比智能指针更普遍的是,资源拥有类型,如
vector
soptional
或variant
s。所有这些都是让用户不关心它们的词汇类型。 - 比 1 更通用的对象,其身份很重要。例如,具有外部注册的对象必须在注册存储中重新注册新副本,并且在销毁时必须注销自己。
- 由于并发而必须小心或花哨的情况。例如,如果您有一个
mutex_guarded<T>
模板并且您希望它们是可复制的,则默认复制不起作用,因为包装器具有互斥锁,并且无法复制互斥锁。在其他情况下,您可能需要保证某些操作的顺序,进行比较和设置,甚至跟踪或记录对象的“本机线程”以检测它何时跨越了线程边界。
回答
拥有已经遵循五个规则的良好封装概念确实确保您不必担心它。也就是说,如果您发现自己处于必须编写一些自定义逻辑的情况,它仍然成立。想到的一些事情:
- 您自己的智能指针类型
- 必须注销的观察者
- C 库的包装器
接下来,我发现一旦你有足够的组合,就不再清楚类的行为将是什么。赋值运算符可用吗?我们可以复制构造类吗?因此,强制执行五规则,即使= default
在其中,结合-Wdefaulted-function-deleted as error 有助于理解代码。
要仔细查看您的示例:
这段代码确实可以很好地转换为:
但是,现在想象一下:
class ResourceManager {
ResourcePool &pool;
Resource *resource;
ResourceManager(ResourcePool &pool) : pool{pool}, resource{pool.createResource()} {}
~ResourceManager() { pool.destroyResource(resource);
};
同样,unique_ptr
如果你给它一个自定义析构函数,这可以用 a 来完成。但是,如果您的类现在存储了大量资源,您是否愿意支付额外的内存成本?
如果您首先需要锁定,然后才能将资源返回到池中进行回收怎么办?你会只拿这个锁一次并返回所有资源还是 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