Обработка ошибок в C++: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в C++: от try/catch к современным подходам

Механизм исключений в C++ — это мощный, но требующий глубокого понимания инструмент. В то время как базовое использование try/catch известно большинству разработчиков, профессиональная обработка ошибок включает в себя гораздо больше: от гарантий безопасности исключений до альтернативных стратегий без исключений. В этой статье мы разберем продвинутые техники работы с ошибками в современном C++.

1. Гарантии безопасности исключений: фундамент надежного кода

Любая функция, которая может выбросить исключение, должна предоставлять одну из трех гарантий безопасности:

  • Базовая гарантия (Basic Guarantee): в случае исключения инварианты объекта сохраняются, утечек ресурсов нет. Объект может находиться в непредсказуемом, но валидном состоянии.
  • Строгая гарантия (Strong Guarantee): операция либо полностью выполняется, либо состояние программы остается неизменным (атомарность). Достигается через идиому copy-and-swap.
  • Гарантия отсутствия исключений (Noexcept Guarantee): функция никогда не выбрасывает исключения. Критично для деструкторов, move-операций и операций, используемых в контейнерах.
// Пример строгой гарантии через copy-and-swap
class Vector {
    int* data;
    size_t sz, cap;
public:
    void push_back(int value) {
        if (sz == cap) {
            // 1. Создаем копию
            Vector tmp(*this);
            // 2. Модифицируем копию (может выбросить исключение)
            tmp.reserve(cap * 2);
            // 3. Обмен (noexcept!)
            swap(tmp);
        }
        data[sz++] = value;  // noexcept
    }
    
    void swap(Vector& other) noexcept {
        std::swap(data, other.data);
        std::swap(sz, other.sz);
        std::swap(cap, other.cap);
    }
};

2. RAII как основа управления ресурсами

Идиома RAII (Resource Acquisition Is Initialization) — это не просто умные указатели. Это фундаментальный принцип, позволяющий избежать утечек ресурсов при исключениях:

  • Ресурс захватывается в конструкторе
  • Освобождается в деструкторе (который по умолчанию noexcept)
  • Исключение в деструкторе — UB (неопределенное поведение)
// RAII-обертка для файлового дескриптора
class FileDescriptor {
    int fd;
public:
    explicit FileDescriptor(const char* path) 
        : fd(open(path, O_RDONLY)) {
        if (fd == -1) throw std::runtime_error("Failed to open file");
    }
    
    ~FileDescriptor() noexcept {
        if (fd != -1) close(fd);
    }
    
    // Запрещаем копирование, разрешаем move
    FileDescriptor(FileDescriptor&& other) noexcept : fd(other.fd) {
        other.fd = -1;
    }
};

3. Современные альтернативы исключениям: std::optional и std::expected

Не все ошибки стоит обрабатывать через исключения. Для ожидаемых ошибок (неверный ввод, отсутствие данных) лучше использовать типы-обертки:

#include <optional>
#include <expected>  // C++23

// std::optional — для опциональных значений
std::optional<int> safe_divide(int a, int b) {
    if (b == 0) return std::nullopt;
    return a / b;
}

// std::expected — для ошибок с информацией (C++23)
std::expected<double, std::string> sqrt_checked(double x) {
    if (x < 0) return std::unexpected("Negative input");
    return std::sqrt(x);
}

// Использование
auto result = safe_divide(10, 0);
if (result) {
    std::cout << *result;
} else {
    std::cout << "Division by zero";
}

4. noexcept: спецификация и оптимизации

Спецификатор noexcept — это не просто документация. Он влияет на производительность и поведение стандартной библиотеки:

  • Контейнеры std::vector используют move-семантику только если move-конструктор noexcept
  • noexcept функции могут быть оптимизированы компилятором (меньше кода для раскрутки стека)
  • noexcept деструкторы обязательны (иначе — UB при исключении)
class MoveOptimized {
    std::unique_ptr<int[]> data;
public:
    // noexcept — критично для std::vector
    MoveOptimized(MoveOptimized&& other) noexcept 
        : data(std::move(other.data)) {}
    
    void process() noexcept {
        // Обещаем не выбрасывать
        // Компилятор может убрать обработчики исключений
    }
};

5. Практические рекомендации и антипаттерны

На основе многолетнего опыта работы с C++ сформировались следующие best practices:

  • Используйте исключения для exceptional ситуаций — не для обычного потока управления
  • Всегда ловите исключения по константной ссылке (catch(const std::exception& e))
  • Избегайте пустых catch-блоков — они скрывают ошибки
  • Иерархия исключений — наследуйте от std::exception
  • Не выбрасывайте исключения из деструкторов
// Правильная иерархия исключений
class DatabaseError : public std::runtime_error {
    int error_code;
public:
    DatabaseError(const std::string& msg, int code)
        : std::runtime_error(msg), error_code(code) {}
    
    int code() const noexcept { return error_code; }
};

// Использование
try {
    db.query("SELECT ...");
} catch (const DatabaseError& e) {
    std::cerr << "DB error " << e.code() << ": " << e.what();
} catch (const std::exception& e) {
    std::cerr << "Generic error: " << e.what();
}

Заключение

Продвинутая обработка ошибок в C++ — это не просто знание синтаксиса try/catch

Блоги

Рекомендаций по книгам