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

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

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

Начать курс

Продвинутая обработка ошибок в C++: от try/catch к промышленным практикам

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

1. Основы и стандартные ловушки try/catch

Базовый синтаксис знаком каждому:

#include <iostream>
#include <stdexcept>

void dangerousFunction(int value) {
    if (value < 0) {
        throw std::invalid_argument("Value must be non-negative");
    }
    // ... работа с value
}

int main() {
    try {
        dangerousFunction(-5);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Generic error: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception caught" << std::endl;
    }
    return 0;
}

Важные моменты:

  • Ловите по ссылке — catch (const std::exception& e) избегает срезки объектов.
  • Порядок catch-блоков важен: сначала более конкретные исключения, потом — общие.
  • Не злоупотребляйте catch (...) — он скрывает все ошибки, включая критические (например, std::bad_alloc).
  • Никогда не бросайте исключения из деструкторов — это приводит к неопределённому поведению (обычно к аварийному завершению).

2. RAII как альтернатива try/catch для управления ресурсами

RAII (Resource Acquisition Is Initialization) — ключевой паттерн C++, который делает исключения безопасными. Идея: ресурс захватывается в конструкторе и освобождается в деструкторе. Если происходит исключение, деструкторы всех созданных объектов вызываются автоматически (stack unwinding).

#include <fstream>
#include <vector>
#include <stdexcept>

void processFile(const std::string& filename) {
    std::ifstream file(filename);  // RAII: файл откроется в конструкторе
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + filename);
    }

    std::vector<int> data;
    int value;
    while (file >> value) {
        data.push_back(value);
    }
    // Деструктор file закроет файл автоматически,
    // даже если будет исключение (например, bad_alloc при push_back)
}

Преимущества RAII перед ручным try/catch:

  • Не нужно писать finally блоки — деструкторы гарантируют очистку.
  • Код становится более линейным и читаемым.
  • Исключения не приводят к утечкам памяти, файловых дескрипторов и т.д.

3. Гарантии безопасности исключений

В C++ принято выделять три уровня гарантий для функций:

  • No-throw guarantee — функция никогда не бросает исключения. Помечается noexcept.
  • Strong guarantee — если произошло исключение, состояние программы остаётся неизменным (атомарная операция).
  • Basic guarantee — при исключении не происходит утечки ресурсов, объекты остаются в согласованном состоянии, но их значение может измениться.

Пример с strong guarantee (идиома copy-and-swap):

#include <algorithm>  // для std::swap
#include <vector>
#include <stdexcept>

class SafeVector {
    std::vector<int> data_;
public:
    void push_back(int value) {
        // 1. Создаём копию
        std::vector<int> temp = data_;
        // 2. Модифицируем копию (может бросить исключение)
        temp.push_back(value);
        // 3. Если дошли сюда — меняем оригинал
        std::swap(data_, temp);  // noexcept
    }
};

Совет: Всегда документируйте, какую гарантию даёт ваша функция. Это помогает вызывающему коду правильно обрабатывать ошибки.

4. Пользовательские исключения и иерархии

Стандартных исключений (std::runtime_error, std::logic_error и т.д.) часто недостаточно. Создавайте собственные классы, наследующие от std::exception:

#include <stdexcept>
#include <string>

class NetworkException : public std::runtime_error {
    int errorCode_;
public:
    NetworkException(const std::string& msg, int code)
        : std::runtime_error(msg), errorCode_(code) {}
    int errorCode() const noexcept { return errorCode_; }
};

// Использование
try {
    // ... сетевой вызов
    throw NetworkException("Connection timeout", 1001);
} catch (const NetworkException& e) {
    std::cerr << "Network error #" << e.errorCode()
              << ": " << e.what() << std::endl;
} catch (const std::exception& e) {
    std::cerr << "Other error: " << e.what() << std::endl;
}

Рекомендации:

  • Наследуйте от std::runtime_error или std::logic_error, а не напрямую от std::exception.
  • Добавляйте в класс полезные поля (код ошибки, контекст) — это упрощает отладку.
  • Не делайте иерархию слишком глубокой: 2-3 уровня достаточно.

5. Спецификация noexcept и оптимизация

Ключевое слово noexcept сообщает компилятору, что функция не бросает исключения. Это позволяет:

  • Оптимизировать код (убирается код раскрутки стека).

Блоги

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