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,但如果你想要不同的行为,你可以使用不同的智能指针。

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

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

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

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

我在这里错过了什么吗?

回答

规则的全称是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 对象。intXXX_Init()XXX_Close()

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

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


    回答

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

    在少数情况下,您必须实现复制/移动和销毁操作。

    1. 自参考。有时一个对象的部分引用对象的其他部分。当您复制它们时,它们会天真地引用您从中复制的另一个对象。
    2. 智能指针。有理由实现更多的智能指针。
    3. 比智能指针更普遍的是,资源拥有类型,如vectorsoptionalvariants。所有这些都是让用户不关心它们的词汇类型。
    4. 比 1 更通用的对象,其身份很重要。例如,具有外部注册的对象必须在注册存储中重新注册新副本,并且在销毁时必须注销自己。
    5. 由于并发而必须小心或花哨的情况。例如,如果您有一个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


          以上是5法则(对于构造函数和析构函数)是否过时了?的全部内容。
          THE END
          分享
          二维码
          < <上一篇
          下一篇>>