什么是四规则(半规则)?

Posted

技术标签:

【中文标题】什么是四规则(半规则)?【英文标题】:What is the Rule of Four (and a half)? 【发布时间】:2018-01-26 23:56:00 【问题描述】:

为了正确处理对象复制,经验法则是Rule of Three。对于 C++11,移动语义是一回事,所以它是 Rule of Five。但是,在围绕here 和互联网的讨论中,我还看到了对Rule of Four (and a half) 的引用,它是五规则和复制和交换习语的组合。

那么究竟需要实现哪些功能,每个功能的主体应该是什么样的?哪个功能是一半?与五法则相比,这种方法有什么缺点或警告吗?

这是一个类似于我当前代码的参考实现。如果这是不正确的,正确的实现应该是什么样的?

//I understand that in this example, I could just use `std::unique_ptr`.
//Just assume it's a more complex resource.
#include <utility>

class Foo 
public:
    //We must have a default constructor so we can swap during copy construction.
    //It need not be useful, but it should be swappable and deconstructable.
    //It can be private, if it's not truly a valid state for the object.
    Foo() : resource(nullptr) 

    //Normal constructor, acquire resource
    Foo(int value) : resource(new int(value)) 

    //Copy constructor
    Foo(Foo const& other) 
        //Copy the resource here.
        resource = new int(*other.resource);
    

    //Move constructor
    //Delegates to default constructor to put us in safe state.
    Foo(Foo&& other) : Foo() 
        swap(other);
    

    //Assignment
    Foo& operator=(Foo other) 
        swap(other);
        return *this;
    

    //Destructor
    ~Foo() 
        //Free the resource here.
        //We must handle the default state that can appear from the copy ctor.
        //(The if is not technically needed here. `delete nullptr` is safe.)
        if (resource != nullptr) delete resource;
    

    //Swap
    void swap(Foo& other) 
        using std::swap;

        //Swap the resource between instances here.
        swap(resource, other.resource);
    

    //Swap for ADL
    friend void swap(Foo& left, Foo& right) 
        left.swap(right);
    

private:
    int* resource;
;

【问题讨论】:

不需要if (resource != nullptr) delete resource; 中的if 由于您不应该使用原始的、拥有的指针,因此很少需要编写自己的析构函数。坦率地说:我不再相信 0、3、4、5、6 的任何一条规则。我尝试以一种只需要编写尽可能少的特殊成员函数的方式编写我的类。 @MikeMB 这是 0 的规则 @jpfx1342:如果不清楚,抱歉。我的评论是为了回答 4 半规则是什么问题(有时你需要 dtor -> 5,有时你不需要 -> 4)。因为这只回答了问题的一部分,所以我没有回答。 与大多数玩具示例一样,很难得出任何一般性结论。例如,为什么有人会存储 int* 然后想要深拷贝?拥有一个“无用”的默认构造函数似乎不是最好的主意,特别是如果它只是用于一个奇怪的移动构造函数 - 这将在 std::swap 内构造另一个对象。似乎不是移动构造函数应该进行的明显速度优化。此外,当交换移动分配对象时,可能调用std::swap 的赋值运算符似乎是递归的...... 【参考方案1】:

那么究竟什么是四规则(半规则)?

“四大(半)规则”指出,如果您实施其中一个

复制构造函数 赋值运算符 移动构造函数 析构函数 交换功能

那么你必须有关于其他人的政策。

需要实现哪些函数,每个函数的主体应该是什么样子?

默认构造函数(可以是私有的) 复制构造函数(这里有处理资源的真实代码)

移动构造函数(使用默认构造函数和交换):

S(S&& s) : S  swap(*this, s); 

赋值运算符(使用构造函数和交换)

S& operator=(S s)  swap(*this, s); 

析构函数(资源的深拷贝)

朋友交换(没有默认实现:/您可能应该想要交换每个成员)。与交换成员方法相反,这一点很重要:std::swap 使用移动(或复制)构造函数,这将导致无限递归。

一半是哪个函数?

来自上一篇文章:

“要实现 Copy-Swap 习惯用法,您的资源管理类还必须实现一个 swap() 函数来执行逐个成员的交换(有您的“……(半个)”)”

所以swap 方法。

与五法则相比,这种方法有什么缺点或警告吗?

我已经写的警告即将写正确的交换以避免无限递归。

【讨论】:

【参考方案2】:

与五法则相比,这种方法有什么缺点或警告吗?

虽然它可以节省代码重复,但使用复制和交换只会导致更糟糕的类,直言不讳。您正在损害班级的表现,包括移动分配(如果您使用统一分配运算符,我也不喜欢),这应该非常快。作为交换,您将获得强大的异常保证,这起初看起来不错。问题是,您可以通过简单的泛型函数从任何类中获得强大的异常保证:

template <class T>
void copy_and_swap(T& target, T source) 
    using std::swap;
    swap(target, std::move(source));

就是这样。所以需要强异常安全的人无论如何都可以得到它。坦率地说,强大的异常安全性无论如何都是一个小众市场。

真正节省代码重复的方法是通过零规则:选择成员变量,这样您就不需要编写任何的特殊函数。在现实生活中,我会说 90+ % 的时间我看到特殊的成员函数,它们很容易被避免。即使您的类确实具有特殊成员函数所需的某种特殊逻辑,您通常最好将其向下 推入成员中。您的记录器类可能需要在其析构函数中刷新缓冲区,但这不是编写析构函数的理由:编写一个处理刷新的小型缓冲区类并将其作为记录器的成员。记录器可能拥有可以自动处理的各种其他资源,您希望编译器自动生成复制/移动/销毁代码。

关于 C++ 的事情是,自动生成特殊函数是全部或全部,每个函数。那就是复制构造函数(例如)要么自动生成,考虑到 all 成员,要么你必须手动编写(更糟糕的是,维护)它all。所以它强烈地推​​动你向下推。

如果您正在编写一个类来管理资源并需要处理它,它通常应该是:a) 相对较小,b) 相对通用/可重用。前者意味着一些重复的代码没什么大不了的,后者意味着你可能不想把性能放在桌面上。

总之,我强烈反对使用复制和交换,以及使用统一赋值运算符。尝试遵循零法则,如果不能,遵循五法则。写swap 的前提是你可以让它比通用交换(3 次移动)更快,但通常你不必费心。

【讨论】:

你能提供任何关于 4.5 规则比直接规则 5 慢的参考吗?在 Godbolt 上玩,我可以看到生成的代码略有不同,但不清楚是否明显更糟。并不是所有的资源管理类都很小。考虑vector,它通常需要管理自己的资源。 (至少在复制和销毁与就地修改的价格大致相同的情况下,我意识到这并不是所有情况。我的具体激励示例让我今天来到了这个页面是智能指针,无论如何复制都很便宜;对于像vector 这样的东西,它可能是一个额外的分配/释放周期和额外的内存使用。这就是你的意思吗?) @DanielH 延迟很长,但是... 4.5 的规则较慢,因为移动分配比交换操作更小,更受限制。就 3 步而言的交换基本上是最优的,模排序,特定的机器指令等,非常低级的细节。就交换而言的移动分配显然不是最佳的。即使对于像 unique_ptr 这样的东西,它只是引擎盖下的原始指针。移动分配只是一个单一的分配(读旧+写新),另一个写(旧空)。交换是 3 个任务(读新 + 写临时,读旧 + 写新,读临时 + 写旧)。 所以它是 2 次写入 + 1 次读取,而 3 次写入 + 3 次读取。实际情况当然比这更复杂(例如,最后一次读取没有实际成本,因为它已经在寄存器中),编译器可以帮助您并优化事情。但是由于各种原因,很难依赖它,这需要更多时间来解释。基本上,在一天结束时,4.5 的规则只是要求做更多的工作,其中一些是不必要的(但它给了你强有力的例外保证)。【参考方案3】:

简单来说,记住这一点。

0 规则

Classes have neither custom destructors, copy/move constructors or copy/move assignment operators.

3 规则: 如果您实现其中任何一个的自定义版本,您就实现了所有这些。

Destructor, Copy constructor, copy assignment

5 规则: 如果您实现自定义移动构造函数或移动赋值运算符,则需要定义所有 5 个。需要移动语义。

Destructor, Copy constructor, copy assignment, move constructor, move assignment

四个半规则: 与规则 5 相同,但具有复制和交换习语。通过包含 swap 方法,复制赋值和移动赋值合并为一个赋值运算符。

Destructor, Copy constructor, move constructor, assignment, swap (the half part)

Destructor: ~Class();
Copy constructor: Class(Class &);
Move constructor: Class(Class &&);
Assignment: Class & operator = (Class);
Swap: void swap(Class &);

没有警告,优点是赋值更快,因为传值拷贝实际上比在方法体中创建临时对象更有效。

现在我们有了临时对象,我们只需对临时对象执行交换即可。当它超出范围时它会自动销毁,现在我们的对象中的运算符右侧的值。

参考文献

https://www.linkedin.com/learning/c-plus-plus-advanced-topics/rule-of-five?u=67551194 https://en.cppreference.com/w/cpp/language/rule_of_three

【讨论】:

以上是关于什么是四规则(半规则)?的主要内容,如果未能解决你的问题,请参考以下文章

半小时学会正则表达式(上)

关于英语单词元音字母的发音规则

伪静态设置规则

什么叫规则?规则有什么作用?

绘制大量半透明矩形,wpf和GDI哪个效率高?

需求分析文档为什么很难写?(续)