什么是四(半)法则?

What is the Rule of Four (and a half)?

为了正确处理对象复制,经验法则是 Rule of Three. With C++11, move semantics are a thing, so instead it's the Rule of Five. However, in discussions around here and on the internet, I've also seen references to the 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;
};

So what exactly is the Rule of Four (and a half)?

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

  • 拷贝构造函数
  • 赋值运算符
  • 移动构造函数
  • 析构函数
  • 交换功能

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

Which functions need to implemented, and what should each function's body look like?

  • 默认构造函数(可以是私有的)
  • 复制构造函数(这里你有真正的代码来处理你的资源)
  • 移动构造函数(使用默认构造函数和交换):

    S(S&& s) : S{} { swap(*this, s); }
    
  • 赋值运算符(使用构造函数和交换)

    S& operator=(S s) { swap(*this, s); }
    
  • 析构函数(资源的深拷贝)

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

Which function is the half?

来自上一篇文章:

"To implement the Copy-Swap idiom your resource management class must also implement a swap() function to perform a member-by-member swap (there’s your “…(and a half)”)"

所以 swap 方法。

Are there any disadvantages or warnings for this approach, compared to the Rule of Five?

我已经写过的警告是关于编写正确的交换以避免无限递归。

Are there any disadvantages or warnings for this approach, compared to the Rule of Five?

虽然它可以节省代码重复,但使用复制和交换只会导致更糟糕的 classes,说白了。你正在损害你的 class' 性能,包括移动赋值(如果你使用统一赋值运算符,我也不喜欢),这应该非常快。作为交换,您将获得强大的异常保证,乍一看这似乎不错。问题是,您可以使用简单的通用函数从任何 class 获得强大的异常保证:

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

就是这样。所以需要强异常安全的人无论如何都能搞定。坦率地说,无论如何,强大的异常安全性都是一个利基市场。

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

关于 C++ 的事情是,每个函数自动生成特殊函数是全有或全无。那就是复制构造函数(例如)要么自动生成,考虑到 all 成员,要么你必须编写(更糟糕的是,维护)它 all 手动。因此,它会强烈推动您采用向下推动的方法。

如果您正在编写 class 来管理资源并需要处理这个问题,它通常应该:a) 相对较小,b) 相对 generic/reusable。前者意味着一些重复的代码没什么大不了的,而后者意味着您可能不想将性能留在 table.

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

简单来说,记住这个就行了。

规则 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