Обработка ошибок в C++: try/catch и не только — полное руководство

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

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

Начать курс

Введение в обработку ошибок в C++

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

1. Классический подход: конструкция try/catch

Основной механизм обработки исключений в C++ строится на трёх ключевых словах: try, catch и throw. Исключение — это объект любого типа, который «выбрасывается» при возникновении ошибки и «ловится» в соответствующем блоке.

#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0) {
        throw std::runtime_error("Деление на ноль!");
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10, 0);
        std::cout << "Результат: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "Ошибка: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Неизвестная ошибка!" << std::endl;
    }
    return 0;
}

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

  • Иерархия исключений: рекомендуется выбрасывать объекты классов, унаследованных от std::exception (например, std::runtime_error, std::logic_error).
  • Перехват по ссылке: всегда ловите исключения по константной ссылке (const std::exception&), чтобы избежать срезки и лишнего копирования.
  • Универсальный обработчик: catch (...) ловит любые исключения, но не даёт доступа к объекту ошибки. Используйте его только как крайнюю меру.

2. Альтернативные подходы: коды возврата и std::optional

Не всегда исключения — лучший выбор. В высоконагруженных системах или встроенном программировании часто предпочитают более лёгкие механизмы.

2.1. Коды возврата (error codes)

Функция возвращает целое число или перечисление, указывающее на успех или тип ошибки. Этот метод прост и эффективен, но требует явной проверки результата.

enum class ErrorCode { Success, DivisionByZero, Overflow };

ErrorCode safe_divide(double a, double b, double& result) {
    if (b == 0.0) return ErrorCode::DivisionByZero;
    result = a / b;
    return ErrorCode::Success;
}

int main() {
    double res;
    ErrorCode err = safe_divide(10, 0, res);
    if (err != ErrorCode::Success) {
        // Обработка ошибки
    }
}

2.2. std::optional (C++17)

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

#include <optional>

std::optional<double> divide_opt(double a, double b) {
    if (b == 0.0) return std::nullopt;
    return a / b;
}

int main() {
    auto result = divide_opt(10, 2);
    if (result) {
        std::cout << *result << std::endl;
    } else {
        std::cout << "Ошибка деления" << std::endl;
    }
}

3. Современные возможности: noexcept и std::expected (C++23)

Стандарт C++ продолжает развиваться, предлагая всё более выразительные средства для работы с ошибками.

3.1. Спецификация noexcept

Ключевое слово noexcept указывает, что функция не выбрасывает исключений. Это помогает компилятору оптимизировать код и делает интерфейс более понятным.

void log_message(const std::string& msg) noexcept {
    // Простая запись в лог — исключений быть не должно
    std::cout << msg << std::endl;
}

// Проверка в compile-time
static_assert(noexcept(log_message("test")), "Функция должна быть noexcept");

Если noexcept-функция всё же выбрасывает исключение, программа аварийно завершается вызовом std::terminate.

3.2. std::expected (C++23)

Этот новый тип из библиотеки <expected> представляет собой объединение ожидаемого значения и ошибки. Он более гибок, чем std::optional, так как позволяет хранить информацию об ошибке.

#include <expected>
#include <string>

enum class MathError { DivByZero, NegativeInput };

std::expected<double, MathError> sqrt_safe(double x) {
    if (x < 0) return std::unexpected(MathError::NegativeInput);
    return std::sqrt(x);
}

int main() {
    auto res = sqrt_safe(-4);
    if (res) {
        // Используем *res
    } else {
        // Обрабатываем res.error()
    }
}

4. Best practices: когда что использовать?

Выбор стратегии обработки ошибок зависит от контекста. Приведём краткие рекомендации:

  • Исключения (try/catch): используйте для исключительных ситуаций, которые нарушают нормальный ход программы (ошибки ввода-вывода, нехватка памяти). Исключения не должны быть частью обычного потока управления.
  • Коды возврата и std::optional: идеальны для частых, ожидаемых ошибок (неверный ввод пользователя, отсутствие файла). Они быстрее и не нагружают стек.
  • noexcept: помечайте функции, которые гарантированно не выбрасывают исключений. Это улучшает оптимизацию и читаемость.
  • std::expected: выбирайте для функций, которые могут вернуть как результат, так и детализированную ошибку без использования исключений.

Общее правило: старайтесь не смешивать разные подход

Блоги

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