
I was quickly looking at LinkedIn, and found a post with a few good questions about the rule of five. I started answering them, wrote a relatively long answer in a minute, and then the app crashed.
Everything was lost. So I decided to take a small break, and write a blog post instead.
The questions were:
- Is it always necessary to implement all five functions?
- When should you delete copy operations but allow move?
- What is Rule of Zero and when should it be preferred?
- Can Rule of Five be harmful?
- Why do STL containers require move constructors to be noexcept?
- What happens if your move constructor throws?
- How does Rule of Five interact with custom allocators?
I assume you’re already familiar with the rule of five in depth. Internet is full of resources explaining the basic principle, but the general idea is to always specify, in a class, a copy-constructor, a copy-assignment operator, a move constructor, a move-assignment operator, and a destructor.
Let’s go.
Is it always necessary to implement all five functions?
The short answer is, as always, “it depends”.
We are assuming the question is “can I implement between 1 and 4 of those member functions”, because if you’re defining none, you’re applying the rule of zero, which has its own reasons.
The rule of five is a nice mnemonic rule to make sure you’re doing “the right thing”, and force you to think what you’re doing.
If your codebase is only maintained by you, or by very experienced developer who know intuitively all the rules of generating constructors, and you’re always checking against every time you add or remove other functions that might potentially modify the automatic generation of such members, no – you don’t need to.
Since nobody’s perfect, sometime your core team members might be tired, distracted or have a new maintainer who’s less experienced. Applying the rule of five might help.
What I suggest is to default members when in doubt, or when delete is implied by other conditions (e.g. the copy constructors are implicitly deleted if you have a non-copyable member such as unique_ptr).
struct A {
A(const A&) = default; // implicitly deleted
A& operator=(const A&) = default; // implicitly deleted
A(A&&) = default;
A& operator=(A&&) = default;
~A() = default;
private:
std::unique_ptr<B> b_;
};
When should you delete copy operations but allow move?
The answer is simple: when your class is only moveable.
But again, note what I said above – I prefer an explicit defaulting resulting implicit deletion. Why? Because today you might be having resources managed by unique_ptrs, but tomorrow you might decide to change the ownership model. defaulting will allow your class to automatically follow your new decision, while an explicit delete on the copy constructor and copy-assignment operator will require additional updates.
struct A {
A(const A&) = default; // previously deleted, now valid
A& operator=(const A&) = default; // previously deleted, now valid
A(A&&) = default;
A& operator=(A&&) = default;
~A() = default;
private:
std::shared_ptr<B> b_;
};
You should definitely delete copy operations if you want to have move-only semantics, and there’s nothing to imply that semantics (i.e. you don’t have other non-copyable members, and you don’t inherit from a non-copyable class).
What is Rule of Zero and when should it be preferred?
I would make this answer short and say: “if, when applying the rule of five, you end up = defaulting all the five functions, you should remove everything and use the rule of zero”.
But there might be cases when it looks like you’re defaulting everything even if you’re not, one notable example is when you want a virtual destructor:
struct A {
A(const A&) = default;
A& operator=(const A&) = default;
A(A&&) = default;
A& operator=(A&&) = default;
virtual ~A() = default;
};
In the example above, you’re not really defaulting the destructor, you’re also declaring that the destructor is virtual. So, in this case, you should still apply the rule of five, not the rule of zero.
Can Rule of Five be harmful?
Partially yes. Let’s go back to the first example, and put the explicit delete:
struct A {
A(const A&) = delete; // explicitly deleted
A& operator=(const A&) = delete; // explicitly deleted
A(A&&) = default;
A& operator=(A&&) = default;
~A() = default;
private:
std::unique_ptr<B> b_;
};
And let’s sloppily reapply the change of ownership mechanism
struct A {
A(const A&) = delete; // still deleted
A& operator=(const A&) = delete; // still deleted
A(A&&) = default;
A& operator=(A&&) = default;
~A() = default;
private:
std::shared_ptr<B> b_;
};
Now your class A could be copyable, but it isn’t, because you forgot to “undelete” the copy constructor and copy-assignment operator.
Why do STL containers require move constructors to be noexcept?
This isn’t about the rule of five, but it’s a simple answer: if they weren’t, they should guarantee exception safety, and ensure a consistent state (either “moved” or “not moved”) in case of exceptions.
Problem is, they can’t. If you start moving all the elements one by one, and one in the middle raises an exception, what can you do? If you decide to rethrow, you’re in a state where some of the elements have been moved and some didn’t. You might lose some elements, leak resources, or turn your PC into an upside-down bistrot.
What happens if your move constructor throws?
Per se, nothing. move constructors are like normal constructors, your new object isn’t constructed.
The real problem is “how did you code your move constructor to deal with exceptions”? If your move constructor throws, you should really guarantee that your original object hasn’t been partially invalidated, and that might be tricky. There’s no general rule, but requiring the sub-objcets to be nothrow-moveable will ensure you’ll be able to move them back in case of exception (see previous point).
How does Rule of Five interact with custom allocators?
Here I must say, I have no idea.
I have used custom allocators in the past, but my experience is definitely not enough to explain how they interact. Unfortunately this would take more than the time I wanted to allocate, and my five-minutes break is over.