Правило трёх/пяти/ноля
Правило трёх
Если классу требуется пользовательский деструктор, пользовательский конструктор копирования или пользовательский оператор присваивания копированием, он почти наверняка требует все три.
Поскольку C++ копирует и копирует при присваивании объекты пользовательских типов в различных ситуациях (передача/возврат по значению, управление контейнером и т.д.), эти специальные функции-элементы будут вызываться, если они доступны, и если они не определены пользователем, они неявно определяются компилятором.
Неявно определённые специальные функции-элементы обычно неверны, если класс управляет ресурсом, дескриптор которого является объектом неклассового типа (сырой указатель, файловый дескриптор POSIX и т.д.), чей деструктор ничего не делает, а конструктор копирования/оператор присваивания выполняет "поверхностное копирование" (копирование значения дескриптора без дублирования базового ресурса).
#include <cstddef>
#include <cstring>
#include <iostream>
class rule_of_three
{
char* cstring; // сырой указатель, используемый в качестве дескриптора
// для динамически выделяемого блока памяти
rule_of_three(const char* s, std::size_t n) // чтобы не считать дважды
: cstring(new char[n]) // выделяет память
{
std::memcpy(cstring, s, n); // заполняет
}
public:
explicit rule_of_three(const char* s = "")
: rule_of_three(s, std::strlen(s) + 1) {}
~rule_of_three() // I. деструктор
{
delete[] cstring; // освобождает память
}
rule_of_three(const rule_of_three& other) // II. конструктор копирования
: rule_of_three(other.cstring) {}
rule_of_three& operator=(const rule_of_three& other) // III. присваивание копированием
{
if (this == &other)
return *this;
std::size_t n{std::strlen(other.cstring) + 1};
char* new_cstring = new char[n]; // выделяет память
std::memcpy(new_cstring, other.cstring, n); // заполняет
delete[] cstring; // освобождает память
cstring = new_cstring;
return *this;
}
operator const char *() const // доступ
{
return cstring;
}
};
int main()
{
rule_of_three o1{"abc"};
std::cout << o1 << ' ';
auto o2{o1}; // II. использует конструктор копирования
std::cout << o2 << ' ';
rule_of_three o3("def");
std::cout << o3 << ' ';
o3 = o2; // III. использует присваивание копированием
std::cout << o3 << '\n';
} // I. все деструкторы вызываются 'здесь'
Вывод:
abc abc def abc
Классы, которые управляют некопируемыми ресурсами через копируемые дескрипторы, возможно, должны объявить присваивание копированием и конструктор копирования закрытыми и не предоставлять их определения или определять их удалёнными. Это ещё одно применение правила трёх: удаление одного и оставление другого неявно определённым, скорее всего, приведёт к ошибкам.
Правило пяти
Поскольку наличие определяемого пользователем (объявленного или = default или = delete) деструктора, конструктора копирования или оператора присваивания копированием предотвращает неявное определение конструктора перемещения и оператора присваивания перемещением, любой класс, для которого желательна семантика перемещения, должен объявить все пять специальных функций-элементов:
class rule_of_five
{
char* cstring; // сырой указатель, используемый в качестве дескриптора
// для динамически выделяемого блока памяти
public:
explicit rule_of_five(const char* s = "") : cstring(nullptr)
{
if (s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // распределение памяти
std::memcpy(cstring, s, n); // заполнение
}
}
~rule_of_five()
{
delete[] cstring; // освобождение памяти
}
rule_of_five(const rule_of_five& other) // конструктор копирования
: rule_of_five(other.cstring) {}
rule_of_five(rule_of_five&& other) noexcept // конструктор перемещения
: cstring(std::exchange(other.cstring, nullptr)) {}
rule_of_five& operator=(const rule_of_five& other) // присваивание копированием
{
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept // присваивание перемещением
{
std::swap(cstring, other.cstring);
return *this;
}
// в качестве альтернативы замените оба оператора присваивания на
// rule_of_five& operator=(rule_of_five other) noexcept
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};
В отличие от Правила Трёх, отсутствие конструктора перемещения и присваивания перемещением обычно является не ошибкой, а упущенной возможностью оптимизации.
Правило нуля
Классы, которые имеют собственные деструкторы, конструкторы копирования/перемещения или операторы присваивания копированием/перемещением, должны иметь дело исключительно с владением (что следует из Принципа Единственной Ответственности). Другие классы не должны иметь настраиваемых деструкторов, конструкторов копирования/перемещения или операторов присваивания копированием/перемещением[1].
Это правило также появляется в Основных Рекомендациях C++ как C.20: Если вы можете избежать определения операций по умолчанию, сделайте это.
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
Когда базовый класс предназначен для полиморфного использования, его деструктор может быть объявлен открытым и виртуальным. Это блокирует неявные перемещения (и объявляет устаревшими неявные копирования), поэтому специальные функции-элементы должны быть объявлены как используемые по умолчанию[2].
class base_of_five_defaults
{
public:
base_of_five_defaults(const base_of_five_defaults&) = default;
base_of_five_defaults(base_of_five_defaults&&) = default;
base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
virtual ~base_of_five_defaults() = default;
};
Однако это делает класс склонным к срезке, поэтому полиморфные классы часто определяют копирование удалённым (смотрите C.67: Полиморфный класс должен подавлять открытое копирование/перемещение в Основных Рекомендациях C++), что приводит к следующей общей формулировке правила пяти: