C++ The Rule of Five
The rule of five is a guideline for classes that own a resource. It says a class that needs one of five special methods usually needs all five.
Copies print their counter line
The Text class owns a heap int through a raw pointer. One copy construction and one copy assignment run here, and the counter line prints Copies: 2.
#include <iostream>
#include <string>
#include <utility>
class Text {
public:
static int copies;
static int moves;
std::string word;
int* len;
Text(std::string w) {
word = w;
len = new int(static_cast<int>(w.size()));
}
~Text() {
delete len;
}
Text(const Text& other) {
copies++;
word = other.word;
len = new int(*other.len);
}
Text& operator=(const Text& other) {
if (this != &other) {
copies++;
word = other.word;
*len = *other.len;
}
return *this;
}
Text(Text&& other) {
moves++;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
Text& operator=(Text&& other) {
if (this != &other) {
moves++;
delete len;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
return *this;
}
void print() {
std::cout << word << " (" << *len << ")" << std::endl;
}
};
int Text::copies = 0;
int Text::moves = 0;
int main() {
Text a("Hello");
Text b = a;
Text c("World");
c = a;
b.print();
c.print();
std::cout << "Copies: " << Text::copies << std::endl;
return 0;
}Moves print their counter line
One move construction and one move assignment run here. Each move takes the heap int and leaves its source with none, and the counter line prints Moves: 2.
#include <iostream>
#include <string>
#include <utility>
class Text {
public:
static int copies;
static int moves;
std::string word;
int* len;
Text(std::string w) {
word = w;
len = new int(static_cast<int>(w.size()));
}
~Text() {
delete len;
}
Text(const Text& other) {
copies++;
word = other.word;
len = new int(*other.len);
}
Text& operator=(const Text& other) {
if (this != &other) {
copies++;
word = other.word;
*len = *other.len;
}
return *this;
}
Text(Text&& other) {
moves++;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
Text& operator=(Text&& other) {
if (this != &other) {
moves++;
delete len;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
return *this;
}
void print() {
std::cout << word << " (" << *len << ")" << std::endl;
}
};
int Text::copies = 0;
int Text::moves = 0;
int main() {
Text a("Hello");
Text b = std::move(a);
Text c("World");
c = std::move(b);
c.print();
std::cout << "Moves: " << Text::moves << std::endl;
return 0;
}Self-assignment is guarded
Each assignment checks whether the object is itself first. Both self-assignments do nothing, both objects keep their values, and neither counter moves.
#include <iostream>
#include <string>
#include <utility>
class Text {
public:
static int copies;
static int moves;
std::string word;
int* len;
Text(std::string w) {
word = w;
len = new int(static_cast<int>(w.size()));
}
~Text() {
delete len;
}
Text(const Text& other) {
copies++;
word = other.word;
len = new int(*other.len);
}
Text& operator=(const Text& other) {
if (this != &other) {
copies++;
word = other.word;
*len = *other.len;
}
return *this;
}
Text(Text&& other) {
moves++;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
Text& operator=(Text&& other) {
if (this != &other) {
moves++;
delete len;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
return *this;
}
void print() {
std::cout << word << " (" << *len << ")" << std::endl;
}
};
int Text::copies = 0;
int Text::moves = 0;
int main() {
Text a("Hello");
a = a;
a.print();
Text b("World");
b = std::move(b);
b.print();
std::cout << "Copies: " << Text::copies << " Moves: " << Text::moves << std::endl;
return 0;
}Example
#include <iostream>
#include <string>
#include <utility>
class Text {
public:
static int copies;
static int moves;
std::string word;
int* len;
Text(std::string w) {
word = w;
len = new int(static_cast<int>(w.size()));
}
~Text() {
delete len;
}
Text(const Text& other) {
copies++;
word = other.word;
len = new int(*other.len);
}
Text& operator=(const Text& other) {
if (this != &other) {
copies++;
word = other.word;
*len = *other.len;
}
return *this;
}
Text(Text&& other) {
moves++;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
Text& operator=(Text&& other) {
if (this != &other) {
moves++;
delete len;
word = std::move(other.word);
len = other.len;
other.len = nullptr;
}
return *this;
}
void print() {
std::cout << word << " (" << *len << ")" << std::endl;
}
};
int Text::copies = 0;
int Text::moves = 0;
int main() {
Text draft("Draft");
Text copy = draft;
copy.print();
Text finalText("Empty");
finalText = std::move(copy);
finalText.print();
std::cout << "Copies: " << Text::copies << std::endl;
std::cout << "Moves: " << Text::moves << std::endl;
return 0;
}Key points
- The five special methods are the destructor, the copy constructor, copy assignment, the move constructor, and move assignment.
- A class that owns a resource through a raw pointer usually needs a written destructor.
- A written destructor is a sign that the default copies would share the resource.
- Copy assignment reuses or frees the old resource before it takes the new value.
- Move operations leave the source with no resource, so its destructor stays safe.
- A self-assignment guard keeps an object intact when it is assigned to itself.
- Holding only value members is called the rule of zero.
In this example, the copy of draft prints Draft (5) from its own heap int. The move assignment then hands that heap int to finalText. The counters close at one copy and one move.
Note
A class that follows the rule of zero needs none of the five methods written by hand.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A class owns memory through a raw pointer and writes a destructor. What does the rule of five suggest?