Что такое гарантии безопасности исключений (exception safety) в C++?

Ответ

Гарантии безопасности исключений (exception safety) — это свойства функции или метода, описывающие, как её поведение влияет на состояние программы при возникновении исключения. Выделяют три основные гарантии (от слабой к сильной):

  1. Базовая гарантия (Basic Guarantee): При выбросе любого исключения программа остаётся в валидном состоянии. Не происходит утечек ресурсов, инварианты объектов сохраняются, но состояние может измениться (например, объект может остаться в каком-то промежуточном, но корректном состоянии).

  2. Строгая гарантия (Strong Guarantee): Операция обладает транзакционной семантикой. Если операция завершается исключением, состояние программы остаётся точно таким же, как и до её начала. Это часто реализуется через идиому "copy-and-swap".

  3. Гарантия отсутствия исключений (No-throw Guarantee): Операция никогда не генерирует исключений. Это критически важно для деструкторов и функций освобождения ресурсов (например, operator delete).

Пример реализации строгой гарантии с помощью "copy-and-swap":

class String {
    char* data;
    size_t size;
public:
    void append(const char* str) {
        // 1. Создаём копию текущего состояния
        String temp(*this);
        // 2. Модифицируем копию (если здесь выбросится исключение, *this не пострадает)
        temp.internalAppend(str);
        // 3. Атомарно меняем состояние текущего объекта на новое
        swap(temp); // swap обычно noexcept
    }
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
    // ...
};

Практическое правило: Деструкторы, операции swap, move должны обеспечивать no-throw guarantee. Ключевые операции следует стремиться делать либо строго безопасными, либо, как минимум, обеспечивающими базовую гарантию.

Ответ 18+ 🔞

А, блядь, вот эта тема — про гарантии исключений! Ну, ёпта, это ж основа основ, без этого в плюсах как без рук, а точнее, как без головы. Сейчас объясню на пальцах, только не ори, что сложно.

Представь, ты пишешь функцию, а внутри неё может вылететь какая-нибудь манда с ушами, то есть исключение. Так вот, эти гарантии — они про то, что останется после этого срача в твоей программе.

Первая, самая простая — базовая гарантия. Это как минимум, ниже неё опускаться — это уже распиздяйство полное. Суть: если твоя функция накрылась медным тазом и выкинула исключение, программа не должна превратиться в тыкву. Утечек памяти нет, объекты не превращаются в хуй в пальто (то есть в невалидное состояние), они просто могут остаться в каком-то другом, но всё ещё осмысленном состоянии. Не идеально, но жить можно. Без этой гарантии — это доверия ебать ноль к твоему коду.

Вторая, круче — строгая гарантия. Это уже высший пилотаж. Тут всё просто: либо операция прошла на ура, либо — если что-то пошло не так — будто ничего и не было. Вообще. Состояние программы откатывается ровно к тому, что было до вызова. Как будто ты нажал Ctrl+Z. Это транзакционность, мать её. Часто делается через приём «скопировал, поменял, подменил» (copy-and-swap).

Ну и третья, самая жёсткая — гарантия «не кидаю исключений» (no-throw). Тут название говорит само за себя. Функция либо работает, либо не компилируется, но исключений — терпения ноль ебать — не бросает никогда. Это святое для деструкторов и для операций, которые освобождают ресурсы. Если деструктор начнёт швыряться исключениями — это пиздопроебибна ситуация, ведущая прямиком к аду и крешам.

Вот, смотри, как эту строгую гарантию на коленке делают, тот самый copy-and-swap:

class String {
    char* data;
    size_t size;
public:
    void append(const char* str) {
        // 1. Делаем полную копию себя любимого
        String temp(*this);
        // 2. Ковыряемся уже с копией. Если тут всё пойдёт по пизде — оригинальный объект (*this) не пострадает ни на йоту.
        temp.internalAppend(str);
        // 3. Если дожили сюда — атомарно меняем старый state на новый. swap почти всегда noexcept.
        swap(temp); // swap обычно noexcept
    }
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
    // ...
};

Видишь логику? Всё опасное делается с копией. Не получилось — ну и да похуй, оригинал цел. Получилось — быстренько подменили ядра местами. Красота.

И главное практическое правило, которое вбивают с первого дня: деструкторы, функции swap и move-операции обязаны быть no-throw. На них вся безопасность держится. Остальные ключевые операции надо выжимать хотя бы до строгой гарантии, ну или уж ебушки-воробушки, хотя бы базовую обеспечь, а не бзди как попало.