Table of contents
Open Table of contents
Preface
If you can’t explain it simply, you don’t understand it well enough. - Albert Einstein
Something became apparent to me recently while doing pull requests
, that although I had done RAII topics at University, it had become quite difficult for me to explain and even more difficult to apply the more specific rules of zero/three and five. So it seems like a good way to brush up on things I have become rusty on by trying to explain them as simply as possible.
Copy and Move
As simply as can be described what does it mean to copy or to move an object?
Although it might sound obvious from the names, understanding the lifecycle of an object often isn’t. So knowing what a copy
might do vs move
, is important when defining none-straightforward classes.
Therefore, a copy
takes the contents from one object (the data) and makes or populates another element with that data but the two objects are distinct. Updating one object would not update the second. Move
, on the otherhand, takes that data and puts it in the new object (the ownership of that data is transferred). Thus if you move
an object and then modify either of the references to it, both objects would reflect those changes.
Which is better?
Well move
-ing is generally cheap, you take one object and put it or reference it somewhere else. But you need to remember to de-reference the original object (remove its ownership). Copy
on the otherhand requires manually moving each piece of data into a completely different memory location, this is much more time consuming to do. [1]
Assignment and Construction
So the next logical step is to define what assignment and construction of an object is. Both copy
and move
have the relevant assignment and construction operations but what does this mean? Simply, it is either construction of a new object, when it is being constructed (copy/move construction
) whereas assignment uses the =
operator and replaces the contents of an already created object with another object.
We can have either copy
or move
variations of either of these operations. So we can either copy in an already existing object to construct a new object (leaving the old one) or move in that object (and the old one would be effectively deleted) copy construction
and move construction
respectively. Or we could use the =
sign (assignment) to copy in the contents of an object (leaving the old one) or move in an object (removing the old one) again copy assignment
and move assignment
.
Rule of Zero/Three/Five
How does this all play out with classes? Generally, if you can avoid defining custom behaviour for the above copy/move
operations (and destructors
) then do (see here and here). This consitutes the rule of zero, which is an ominous way of saying we haven’t defined any of the copy/move
functions or a custom destructor
.
class zero
{
std::string m_someString;
public:
zero(const std::string& arg) : m_someString(arg) {}
};
*Note: Even with a custom constructor we don’t need to define any of the other methods…
Three/Five
Somewhat self-explanatory therefore, are the rules of three and five, where once we define some of those methods we must define some or all of the methods respectively. The rule of three applies to the copy
operations. So if your class requires custom/user-defined behaviour in any of the destructor or copy operations then you should define all three.
- Copy Construction
- Copy Assignment
- (custom) Destructor
class three
{
char* m_cstring; // raw pointer used as a handle to a
// dynamically-allocated memory block
three(const char* s, std::size_t n) // to avoid counting twice
: m_cstring(new char[n]) // allocate
{
std::memcpy(m_cstring, s, n); // populate
}
public:
explicit three(const char* s = "")
: three(s, std::strlen(s) + 1) {}
~three() // destructor
{
delete[] m_cstring; // deallocate
}
three(const three& other) // copy constructor
: three(other.m_cstring) {}
three& operator=(const three& other) // copy assignment
{
if (this == &other) // CHECK THESE OBJECTS AREN'T THE SAME
return *this;
std::size_t n{std::strlen(other.m_cstring) + 1};
char* new_m_cstring = new char[n]; // allocate space to temporary store
std::memcpy(new_m_cstring, other.m_cstring, n); // populate
delete[] m_cstring; // deallocate OLD m_cstring
m_cstring = new_m_cstring;
return *this;
}
};
Five
Finally, the rule of five means that all copy
and move
operations must be defined (and a custom destructor). This is a (relatively) new addition or extension to the rule of three. But it doesn’t mean that if you missed the move
methods there would be a compilation error, instead your class would end up using the copy
methods and possibly be less efficient.
If you have defined a destructor
, copy constructor
or copy assignment
then the compiler will not provide the class with any move
methods. Additionally if you use = default
or = delete
. Therefore the class will fall back to the copy
methods if no move
methods are provided. So if there would be a performance benefit it may be worth implementing them!
class five
{
char* m_cstring; // raw pointer used as a handle to a
// dynamically-allocated memory block
public:
explicit five(const char* s = "") : m_cstring(nullptr)
{
if (s)
{
std::size_t n = std::strlen(s) + 1;
m_cstring = new char[n]; // allocate
std::memcpy(m_cstring, s, n); // populate
}
}
~five()
{
delete[] m_cstring; // deallocate
}
five(const five& other) // copy constructor
: five(other.m_cstring) {}
five(five&& other) noexcept // move constructor
: m_cstring(std::exchange(other.m_cstring, nullptr)) {}
five& operator=(const five& other) // copy assignment
{
return *this = five(other);
}
five& operator=(five&& other) noexcept // move assignment
{
std::swap(m_cstring, other.m_cstring);
return *this;
}
};
Final Notes
Generally if you stick with classes in the standard library and managed pointers you shouldn't
need to worry about the lifetime of your resources. That should be handled for you. However knowing the rule of zero/three/five is much more important when you have to handle items such as raw pointers in which case it becomes your responsibility to know when and where your data is. Therefore once you start to manage your object more precisely this is when these rules become poignant and worthwhile baring in mind.