Spec-Zone.ru › C++

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

Правило трёх

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

Поскольку C++ копирует объекты пользовательских типов и присваивает им значения в различных ситуациях (передача/возврат по значению, работа с контейнером и т. д.), эти специальные функции-члены будут вызваны, если они доступны. Если же они не определены пользователем, компилятор определит их неявно.

Не следует использовать неявно определённые специальные функции-члены, если класс управляет ресурсом, дескриптор которого имеет тип, не являющийся типом класса (сырой указатель, дескриптор файла POSIX и т. д.), деструктор ничего не делает, а конструктор копирования/оператор присваивания выполняет «поверхностное копирование» (копирует значение дескриптора, не дублируя лежащий в его основе ресурс).

#include <cstddef>
#include <cstring>
#include <iostream>
#include <utility>

class rule_of_three
{
    char* cstring; // raw pointer used as a handle to a
                   // dynamically-allocated memory block

public:
    explicit rule_of_three(const char* s = "") : cstring(nullptr)
    {   
        if (s)
        {   
            cstring = new char[std::strlen(s) + 1]; // allocate
            std::strcpy(cstring, s); // populate
        }
    }
    
    ~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
    {
        // implemented through copy-and-swap for brevity
        // note that this prevents potential storage reuse
        rule_of_three temp(other);
        std::swap(cstring, temp.cstring);
        return *this;
    }
    
    const char* c_str() const // accessor
    {
        return cstring;
    }
};

int main()
{
    rule_of_three o1{"abc"};
    std::cout << o1.c_str() << ' ';
    auto o2{o1}; // II. uses copy constructor
    std::cout << o2.c_str() << ' ';
    rule_of_three o3("def");
    std::cout << o3.c_str() << ' ';
    o3 = o2; // III. uses copy assignment
    std::cout << o3.c_str() << '\n';
}   // I. all destructors are called here

Вывод:

abc abc def abc

Классам, управляющим некопируемыми ресурсами посредством копируемых дескрипторов, может потребоваться объявить оператор присваивания копированием и конструктор копирования private и не предоставлять их определения(до C++11)определить оператор присваивания копированием и конструктор копирования как = delete(начиная с C++11). Это ещё одно применение правила трёх: удалить одну функцию, оставив другую неявно определяемой, как правило, неправильно.

Правило пяти

Поскольку наличие пользовательского (включая = 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)
        {
            cstring = new char[std::strlen(s) + 1]; // allocate
            std::strcpy(cstring, s); // 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
    {
        // implemented as move-assignment from a temporary copy for brevity
        // note that this prevents potential storage reuse
        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 copy-and-swap
// implementation, which also fails to reuse storage in copy-assignment.
//  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) {}
};

Если базовый класс предназначен для полиморфного использования, его деструктор может потребоваться объявить 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;
};

Однако это делает класс подверженным срезке, поэтому в полиморфных классах копирование часто определяют как = delete (см. C.67: Полиморфный класс должен запрещать открытое копирование/перемещение в основных рекомендациях C++). Это приводит к следующей общей формулировке правила пяти: C.21: Если вы определяете или =delete любую функцию копирования, перемещения или деструктор, определите или =delete их все.

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

  1. «Правило нуля», R. Martinho Fernandes, 15.08.2012
  2. «Опасение по поводу правила нуля», Scott Meyers, 13.03.2014.

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/rule_of_three

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API