The rule of three/five/zero
Содержание |
Правило трёх
Если класс требует пользовательский деструктор , пользовательский конструктор копирования , или пользовательский оператор копирующего присваивания , он почти наверняка требует все три.
Поскольку C++ копирует и копирующе присваивает объекты пользовательских типов в различных ситуациях (передача/возврат по значению, манипуляции с контейнером и т.д.), эти специальные функции-члены будут вызываться, если они доступны, и если они не определены пользователем, они неявно определяются компилятором.
Неявно определённые специальные функции-члены не должны использоваться, если класс управляет ресурсом , дескриптор которого является объектом неклассового типа (сырой указатель, POSIX файловый дескриптор и т.д.), чей деструктор ничего не делает, а конструктор копирования/оператор присваивания выполняют "поверхностное копирование" (копирует значение дескриптора без дублирования базового ресурса).
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // необработанный указатель, используемый как дескриптор // для динамически выделенного блока памяти public: explicit rule_of_three(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // выделить память std::strcpy(cstring, s); // заполнить } } ~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. оператор присваивания копированием { // реализовано через copy-and-swap для краткости // обратите внимание, что это предотвращает потенциальное повторное использование памяти rule_of_three temp(other); std::swap(cstring, temp.cstring); return *this; } const char* c_str() const // метод доступа { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. использует конструктор копирования std::cout << o2.c_str() << ' '; rule_of_three o3("def"); std::cout << o3.c_str() << ' '; o3 = o2; // III. использует оператор присваивания копированием std::cout << o3.c_str() << '\n'; } // I. все деструкторы вызываются здесь
Вывод:
abc abc def abc
Классы, управляющие некопируемыми ресурсами через копируемые дескрипторы, могут потребовать объявить копирующий оператор присваивания и копирующий конструктор private и не предоставлять их определения (до C++11) определить копирующий оператор присваивания и копирующий конструктор как = delete (начиная с C++11) . Это еще одно применение правила трех: удаление одного и оставление другого неявно определенным обычно некорректно.
Правило пяти
Поскольку наличие пользовательского (включая = default или = delete объявленных) деструктора, конструктора копирования или оператора копирующего присваивания предотвращает неявное определение move constructor и move assignment operator , любой класс, для которого желательна семантика перемещения, должен объявлять все пять специальных функций-членов:
class rule_of_five { char* cstring; // необработанный указатель, используемый как дескриптор // для динамически выделенного блока памяти public: explicit rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // выделение памяти std::strcpy(cstring, s); // заполнение { { ~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; { // альтернативно, замените оба оператора присваивания реализацией copy-and-swap, // которая также не позволяет повторно использовать память при копирующем присваивании. // rule_of_five& operator=(rule_of_five other) noexcept // { // std::swap(cstring, other.cstring); // return *this; // } {;
В отличие от Правила трёх, отсутствие перемещающего конструктора и перемещающего оператора присваивания обычно не является ошибкой, а упущенной возможностью оптимизации.
Правило нуля
Классы, имеющие пользовательские деструкторы, конструкторы копирования/перемещения или операторы присваивания копированием/перемещением, должны заниматься исключительно вопросами владения (что следует из Принципа единственной ответственности ). Другие классы не должны иметь пользовательских деструкторов, конструкторов копирования/перемещения или операторов присваивания копированием/перемещением [1] .
Это правило также представлено в C++ Core Guidelines как C.20: Если вы можете избежать определения операций по умолчанию, избегайте .
class rule_of_zero { std::string cppstring; public: rule_of_zero(const std::string& arg) : cppstring( arg) {} };
` и `` сохранен без изменений, как и требовалось. HTML-разметка и атрибуты также сохранены в оригинальном виде.
Когда базовый класс предназначен для полиморфного использования, его деструктор может потребоваться объявить public и virtual . Это блокирует неявные перемещения (и объявляет устаревшими неявные копирования), поэтому специальные функции-члены должны быть определены как = default [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; };
Однако это делает класс подверженным срезу (slicing), поэтому полиморфные классы часто определяют копирование как = delete (см. C.67: A polymorphic class should suppress public copy/move в C++ Core Guidelines), что приводит к следующей общей формулировке Правила пяти: