简体   繁体   中英

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

For properly handling object copying, the rule of thumb is the 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) , which is a combination of the Rule of Five and the copy-and-swap idiom.

So what exactly is the Rule of Four (and a half)? Which functions need to be implemented, and what should each function's body look like? Which function is the half? Are there any disadvantages or warnings for this approach, compared to the Rule of Five?

Here's a reference implementation that resembles my current code. If this is incorrect, what would a correct implementation look like?

//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)?

“The Rule of The Big Four (and a half)" states that if you implement one of

  • The copy constructor
  • The assignment operator
  • The move constructor
  • The destructor
  • The swap function

then you must have a policy about the others.

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

  • default constructor (which could be private)
  • copy constructor (Here you have real code to handle your resource)
  • move constructor (using default constructor and swap) :

     S(S&& s) : S{} { swap(*this, s); } 
  • assignment operator (using constructor and swap)

     S& operator=(S s) { swap(*this, s); } 
  • destructor (deep copy of your resource)

  • friend swap (doesn't have default implementation :/ you should probably want to swap each member). This one is important contrary to the swap member method: std::swap uses move (or copy) constructor, which would lead to infinite recursion.

Which function is the half?

From previous article:

"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)”)"

so the swap method.

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

The warning I already wrote is about to write the correct swap to avoid the infinite recursion.

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

Although it can save code duplication, using copy-and-swap simply results in worse classes, to be blunt. You are hurting your class' performance, including move assignment (if you use the unified assignment operator, which I'm also not a fan of), which should be very fast. In exchange, you get the strong exception guarantee, which seems nice at first. The thing is, that you can get the strong exception guarantee from any class with a simple generic function:

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

And that's it. So people who need strong exception safety can get it anyway. And frankly, strong exception safety is quite a niche anyhow.

The real way to save code duplication is through the Rule of Zero: choose member variables so that you don't need to write any of the special functions. In real life, I'd say that 90+ % of the time I see special member functions, they could have easily been avoided. Even if your class does indeed have some kind of special logic required for a special member function, you are usually better off pushing it down into a member. Your logger class may need to flush a buffer in its destructor, but that's not a reason to write a destructor: write a small buffer class that handles the flushing and have that as a member of your logger. Loggers potentially have all kinds of other resources that can get handled automatically and you want to let the compiler automatically generate copy/move/destruct code.

The thing about C++ is that automatic generation of special functions is all or nothing, per function. That is the copy constructor (eg) either gets generated automatically, taking into account all members, or you have to write (and worse, maintain) it all by hand. So it strongly pushes you to an approach of pushing things downwards.

In cases where you are writing a class to manage a resource and need to deal with this, it should typically be: a) relatively small, and b) relatively generic/reusable. The former means that a bit of duplicated code isn't a big deal, and the latter means that you probably don't want to leave performance on the table.

In sum I strongly discourage using copy and swap, and using unified assignment operators. Try to follow the Rule of Zero, if you can't, follow the Rule of Five. Write swap only if you can make it faster than the generic swap (which does 3 moves), but usually you needn't bother.

In simple terms, just remember this.

Rule of 0 :

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

Rule of 3 : If you implement a custom version of any of these, you implement all of them.

Destructor, Copy constructor, copy assignment

Rule of 5 : If you implement a custom move constructor or the move assignment operator, you need to define all 5 of them. Needed for move semantics.

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

Rule of four and a half : Same as Rule of 5 but with copy and swap idiom. With the inclusion of the swap method, the copy assignment and move assignment merge into one assignment operator.

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 &);

There are no warnings, the advantage is that it is faster in assignment as a pass by value copy is actually more efficient than creating a temporary object in the body of the method.

And now that we have that temporary object, we simply perform a swap on the temporary object. It's automatically destroyed when it goes out of scope and we now have the value from the right-hand side of the operator in our object.

References :

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM