close
Пространства имён
Варианты
Действия

Правило трёх/пяти/ноля

Материал из cppreference.com
 
 
Язык С++
Общие темы
Управление потоком
Операторы условного выполнения
if
Операторы итерации (циклы)
Операторы перехода
Функции
Объявление функции
Выражение лямбда-функции
Спецификатор inline
Спецификации динамических исключений (до C++17*)
Спецификатор noexcept (C++11)
Исключения
Пространства имён
Типы
Спецификаторы
decltype (C++11)
auto (C++11)
alignas (C++11)
Спецификаторы длительности хранения
Инициализация
Выражения
Альтернативные представления
Литералы
Логические - Целочисленные - С плавающей запятой
Символьные - Строковые - nullptr (C++11)
Определяемые пользователем (C++11)
Утилиты
Атрибуты (C++11)
Types
Объявление typedef
Объявление псевдонима типа (C++11)
Casts
Неявные преобразования - Явные преобразования
static_cast - dynamic_cast
const_cast - reinterpret_cast
Выделение памяти
Классы
Свойства функции класса
explicit (C++11)
static
Специальные функции-элементы
Шаблоны
Разное
 

Правило трёх

Если классу требуется пользовательский деструктор, пользовательский конструктор копирования или пользовательский оператор присваивания копированием, он почти наверняка требует все три.

Поскольку 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++), что приводит к следующей общей формулировке правила пяти:

C.21: Если вы определяете или делаете =delete любую операцию по умолчанию, определите или сделайте =delete их все

Внешние ссылки