Что такое правило трёх в C++?

Ответ

Правило трёх (Rule of Three) — это принцип C++, который гласит: если классу требуется явно определённый пользовательский деструктор, конструктор копирования или копирующий оператор присваивания, то, вероятно, ему требуются все три. Это следствие необходимости корректного управления ресурсами (память, файловые дескрипторы, сокеты и т.д.).

Почему это важно? Без явного определения этих методов компилятор сгенерирует их автоматически (поверхностное копирование). Для классов, владеющих сырыми указателями, это приводит к двойному освобождению памяти или утечкам.

Пример нарушения и исправления:

// НАРУШЕНИЕ: Класс с сырым указателем и только деструктором.
class BadString {
    char* data;
public:
    BadString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    ~BadString() { delete[] data; } // Только деструктор!
    // Проблема: компилятор сгенерирует конструктор копирования и оператор присваивания,
    // которые просто скопируют указатель. Два объекта будут указывать на одну область памяти.
};

// СОБЛЮДЕНИЕ ПРАВИЛА ТРЁХ:
class String {
    char* data;
public:
    String(const char* str = "") {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    ~String() { delete[] data; } // 1. Деструктор

    // 2. Конструктор копирования (глубокое копирование)
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // 3. Копирующий оператор присваивания (с проверкой на самоприсваивание)
    String& operator=(const String& other) {
        if (this != &other) { // Защита от самоприсваивания (a = a)
            delete[] data; // Освобождаем старые ресурсы
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        }
        return *this;
    }
};

Современный C++ (C++11 и новее): Правило пяти (Rule of Five) расширяет правило тремя, добавляя конструктор перемещения и перемещающий оператор присваивания для эффективной работы с rvalue-ссылками. Лучшей практикой является использование Правила нуля (Rule of Zero): проектировать классы так, чтобы управление ресурсами делегировалось умным указателям (std::unique_ptr, std::shared_ptr) и стандартным контейнерам, тогда все пять специальных функций будут сгенерированы корректно автоматически.

Ответ 18+ 🔞

А, слушай, вот тебе история про один из главных граблей C++, на которые наступают все новички. Представь себе: пишешь ты класс, там внутри сырой указатель, память выделяешь. Всё вроде работает, пока объект один. А потом начинаешь его копировать — и тут начинается ёперный театр. Программа падает с ошибками доступа к памяти, которые нихуя не понять. А всё почему? Потому что нарушил Правило трёх.

В чём суть, ёпта? Говорит оно вот что: если тебе приспичило в классе явно прописать деструктор, конструктор копирования или копирующий оператор присваивания, то, сука, с вероятностью 99.9% тебе нужны все три штуки сразу. Иначе — пиши пропало.

А нахуя оно надо? Да затем, что компилятор — он не телепат, блядь. Если ты не написал эти методы сам, он сгенерирует их за тебя. И сделает он это по-дурацки: просто скопирует все поля побитово (поверхностное копирование). И если у тебя там был указатель на кучу (new), то теперь два объекта будут тыкать в один и тот же кусок памяти. Один из них умрёт (вызовется деструктор) и освободит память. А второй останется с висячим указателем, и когда он тоже захочет умереть — бабах! — двойное освобождение, краш, и ты сидишь с вопросом «какого хуя?». Либо наоборот, память вообще никто не освободит — утечка. Короче, пиздец.

Смотри, как НЕ НАДО делать:

// Класс-распиздяй, который просит по морде.
class BadString {
    char* data;
public:
    BadString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    ~BadString() { delete[] data; } // О, красава, деструктор написал!
    // А конструктор копирования и оператор =? Нихуя!
    // Компилятор их сгенерит сам, и они просто скопируют указатель 'data'.
    // Жди беды.
};

Вот ты создашь два таких объекта, один скопируешь из другого — и у тебя доверия ебать ноль к стабильности программы.

А вот как надо, по правилу трёх:

// Класс, который уже не манда с ушами.
class String {
    char* data;
public:
    String(const char* str = "") {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    ~String() { delete[] data; } // 1. Деструктор. Чистим за собой.

    // 2. Конструктор копирования. Делаем ГЛУБОКОЕ копирование.
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // 3. Копирующий оператор присваивания. Самое интересное.
    String& operator=(const String& other) {
        if (this != &other) { // Святое: проверка на самоприсваивание (a = a)
            delete[] data;                   // Выкидываем старый хлам.
            data = new char[strlen(other.data) + 1]; // Берём новый.
            strcpy(data, other.data);        // Копируем данные.
        }
        return *this; // Возвращаем себя.
    }
};

Вот теперь всё чётко. Каждый объект владеет своей собственной копией строки в памяти. Умер один — освободил свою память. Живёт второй — трогает только свою. Порядок, ебать колотить.

Что там в современности? С C++11 появились перемещения, и Правило трёх выросло до Правила пяти — добавляются ещё конструктор перемещения и перемещающий оператор присваивания. Но самый крутой подход — это Правило нуля: сделай так, чтобы твоему классу не нужно было вообще писать ни одну из этих пяти специальных функций. Перепоручи всю грязную работу умным указателям (std::unique_ptr) и контейнерам из STL. Пусть они там сами разруливают, а твой класс будет простым и безопасным. Идеал, блядь.