C++ Rule of Five

Introduction

C++ “rule of five” is the rules of thumb in modern C++ for the building of exception-safe code and for formalizing rules on resource management.

In this blog post, I would like to discuss about the “rule of three” for classic C++, “rule of five” for modern C++, and their logics behind.

Rule of Three

The classic C++ has a “rule of three”. If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.

The logic behind the “rule of three” is that if the user ever wants to implement a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, rather than uses the implicit destructor, copy constructor, and copy assignment operator, the object of this class usually has raw pointers pointing to the data on the heap. To handle the data on the heap, user-defined destructor, user-defined copy constructor, and user-defined copy assignment operator are all required.

However, the “rule of three” is not usually enforced by the compiler, and the compiler will not even throw warning if the “rule of three” was not followed in the code. For example, if we intentionally comment out the user-defined destructor and the user-defined copy constructor from the rule_of_three class defined in the “The Rule of Three/Five/Zero”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// rule_of_three.cpp
#include <cstddef>
#include <cstring>
#include <iostream>

class rule_of_three
{
char* cstring; // raw pointer used as a handle to a dynamically-allocated
// memory block
rule_of_three(const char* s, std::size_t n) // to avoid counting twice
: cstring(new char[n]) // allocate
{
std::memcpy(cstring, s, n); // populate
}

public:
rule_of_three(const char* s = "") : rule_of_three(s, std::strlen(s) + 1) {}
// ~rule_of_three() // I. destructor
// {
// delete[] cstring; // deallocate
// }
// rule_of_three(const rule_of_three& other) // II. copy constructor
// : rule_of_three(other.cstring)
// {
// }
rule_of_three& operator=(const rule_of_three& other) // III. copy assignment
{
if (this == &other)
return *this;
std::size_t n{std::strlen(other.cstring) + 1};
char* new_cstring = new char[n]; // allocate
std::memcpy(new_cstring, other.cstring, n); // populate
delete[] cstring; // deallocate
cstring = new_cstring;
return *this;
}

public:
operator const char*() const { return cstring; } // accessor
};

int main()
{
rule_of_three o1{"abc"};
std::cout << o1 << ' ';
auto o2{o1}; // I. uses copy constructor
std::cout << o2 << ' ';
rule_of_three o3("def");
std::cout << o3 << ' ';
o3 = o2; // III. uses copy assignment
std::cout << o3 << ' ';
} // <- II. all destructors are called 'here'

The GCC compiler successfully compiled the code without even throwing a warning.

1
$ g++ rule_of_three.cpp -o rule_of_three -std=c++14 -Wall

Of course, this program is problematic and will encounter problems during runtime.

Rule of Five

Move semantics have been introduced to C++ since C++11. As a result of that the “rule of three” has been extended to “rule of five”. If a class requires a user-defined destructor, a user-defined copy constructor, a user-defined copy assignment operator, a user-defined move constructor, a user-defined move assignment operator, it almost certainly requires all five.

The logic behind the “rule of five” is exactly the same as “rule of three”. It is all about the resource management.

The “rule of five” has been enforced by the compiler to some extent. Since C++11, the implicitly-declared copy constructor and copy assignment will be deleted if the class has a user-defined move constructor or move assignment operator. For example, a CustomString class that follows the “rule of five” has been implemented.

rule_of_five.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <string>

struct CustomString
{
public:
explicit CustomString(std::string n) : name{n} {};
CustomString(const CustomString& rhs) : name{rhs.name}, value{rhs.value} {};
CustomString& operator=(const CustomString& rhs)
{
name = rhs.name;
value = rhs.value;
return *this;
};
CustomString(CustomString&& rhs) noexcept
{
name = std::move(rhs.name);
value = rhs.value;
rhs.value = 0;
};
CustomString& operator=(CustomString&& rhs) noexcept
{
name = std::move(rhs.name);
value = rhs.value;
rhs.value = 0;
return *this;
};

private:
std::string name;
long long value;
};

int main()
{
CustomString string1{"Hello"};
CustomString string2{string1}; // Copy constructor.
CustomString string3{std::move(string1)}; // Move constructor.
CustomString string4 = string1; // Copy assignment.
CustomString string5 = std::move(string1); // Move assignment.
}

If the copy constructor and the copy assignment were commented out,

rule_of_five.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <string>

struct CustomString
{
public:
explicit CustomString(std::string n) : name{n} {};
// CustomString(const CustomString& rhs) : name{rhs.name}, value{rhs.value} {};
// CustomString& operator=(const CustomString& rhs)
// {
// name = rhs.name;
// value = rhs.value;
// return *this;
// };
CustomString(CustomString&& rhs) noexcept
{
name = std::move(rhs.name);
value = rhs.value;
rhs.value = 0;
};
CustomString& operator=(CustomString&& rhs) noexcept
{
name = std::move(rhs.name);
value = rhs.value;
rhs.value = 0;
return *this;
};

private:
std::string name;
long long value;
};

int main()
{
CustomString string1{"Hello"};
CustomString string2{string1}; // Copy constructor.
CustomString string3{std::move(string1)}; // Move constructor.
CustomString string4 = string1; // Copy assignment.
CustomString string5 = std::move(string1); // Move assignment.
}

The implicitly-declared copy constructor and copy assignment have been indeed deleted as expected.

1
2
3
4
5
6
7
8
9
10
11
$ g++ rule_of_five.cpp -o rule_of_five -std=c++14 -Wall
rule_of_five.cpp: In function ‘int main()’:
rule_of_five.cpp:37:33: error: use of deleted function ‘CustomString::CustomString(const CustomString&)’
37 | CustomString string2{string1}; // Copy constructor.
| ^
rule_of_five.cpp:4:8: note: ‘CustomString::CustomString(const CustomString&)’ is implicitly declared as deleted because ‘CustomString’ declares a move constructor or move assignment operator
4 | struct CustomString
| ^~~~~~~~~~~~
rule_of_five.cpp:39:28: error: use of deleted function ‘CustomString::CustomString(const CustomString&)’
39 | CustomString string4 = string1; // Copy assignment.
| ^~~~~~~

It should also be noticed that the program still compiles and runs fine if the move constructor and the move assignment have been deleted.

rule_of_five.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <string>

struct CustomString
{
public:
explicit CustomString(std::string n) : name{n} {};
CustomString(const CustomString& rhs) : name{rhs.name}, value{rhs.value} {};
CustomString& operator=(const CustomString& rhs)
{
name = rhs.name;
value = rhs.value;
return *this;
};
// CustomString(CustomString&& rhs) noexcept
// {
// name = std::move(rhs.name);
// value = rhs.value;
// rhs.value = 0;
// };
// CustomString& operator=(CustomString&& rhs) noexcept
// {
// name = std::move(rhs.name);
// value = rhs.value;
// rhs.value = 0;
// return *this;
// };

private:
std::string name;
long long value;
};

int main()
{
CustomString string1{"Hello"};
CustomString string2{string1}; // Copy constructor.
CustomString string3{std::move(string1)}; // Move constructor.
CustomString string4 = string1; // Copy assignment.
CustomString string5 = std::move(string1); // Move assignment.
}
1
$ g++ rule_of_five.cpp -o rule_of_five -std=c++14 -Wall

This is due to C++11 has to be compatible with the classic C++ which does not have move semantics. Because the rvalues std::move(string1) can be converted to const lvalue, copy constructor and copy assignment were called when the rvalue is encountered if there is no move constructor and move assignment overloading.

References

Author

Lei Mao

Posted on

04-17-2022

Updated on

04-17-2022

Licensed under


Comments