Правило трёх/пяти/нуля
Правило трёх
Если класс требует пользовательского деструктора, пользовательского копирующего конструктора или пользовательского оператора копирующего присваивания, то он практически наверняка требует всех трёх.
Поскольку C++ копирует и присваивает объекты пользовательских типов в различных ситуациях (передача/возврат по значению, работа с контейнером и т. д.), эти специальные члены будут вызываться, если доступны, а если они не определены пользователем, то они неявно определяются компилятором.
Неявно определённые специальные члены, как правило, некорректны, если класс управляет ресурсом, чья ручка является объектом неклассового типа (сырой указатель, дескриптор POSIX-файла и т. д.), чей деструктор ничего не делает, а копирующий конструктор/оператор присваивания выполняет «поверхностную копию» (копирует значение ручки без дублирования подлежащего ресурса).
#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:
explicit 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;
}
operator const char* () const // accessor
{
return cstring;
}
};
int main()
{
rule_of_three o1{"abc"};
std::cout << o1 << ' ';
auto o2{o1}; // II. uses copy constructor
std::cout << o2 << ' ';
rule_of_three o3("def");
std::cout << o3 << ' ';
o3 = o2; // III. uses copy assignment
std::cout << o3 << '\n';
} // I. all destructors are called hereВывод:
abc abc def abc
Классы, управляющие некопируемыми ресурсами с помощью копируемых ручек, могут потребовать объявить оператор копирующего присваивания и копирующий конструктор закрытыми и не предоставлять их определения или определять их как удалённые. Это ещё одно применение правила трёх: удаление одного и оставление другого для неявного определения, скорее всего, приведёт к ошибкам.
Правило пяти
Поскольку наличие пользовательского (или = default или = delete объявленного) деструктора, копирующего конструктора или оператора копирующего присваивания препятствует неявной дефиниции перемещающего конструктора и оператора перемещающего присваивания, любой класс, для которого желательны семантики перемещения, должен объявить все пять специальных членов функции:
class rule_of_five
{
char* cstring; // raw pointer used as a handle to a
// dynamically-allocated memory block
public:
explicit rule_of_five(const char* s = "") : cstring(nullptr)
{
if (s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // allocate
std::memcpy(cstring, s, n); // populate
}
}
~rule_of_five()
{
delete[] cstring; // deallocate
}
rule_of_five(const rule_of_five& other) // copy constructor
: rule_of_five(other.cstring) {}
rule_of_five(rule_of_five&& other) noexcept // move constructor
: cstring(std::exchange(other.cstring, nullptr)) {}
rule_of_five& operator=(const rule_of_five& other) // copy assignment
{
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
{
std::swap(cstring, other.cstring);
return *this;
}
// alternatively, replace both assignment operators with
// 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++), что приводит к следующей общей формулировке Правила пяти: C.21: Если вы определены или =удалены любые функции копирования, перемещения или деструктора, определите или =удалите их все.
Внешние ссылки
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/cpp/language/rule_of_three