Namespaces
Variants

The rule of three/five/zero

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Содержание

Правило трёх

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

Поскольку 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) {}
};
**Примечание:** Весь код C++ внутри тегов `
` и `` сохранен без изменений, как и требовалось. 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), что приводит к следующей общей формулировке Правила пяти:

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

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

  1. "Правило нуля", R. Martinho Fernandes 08/15/2012
  2. "Опасения относительно Правила нуля", Scott Meyers, 3/13/2014 .