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

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

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

Начать курс

Обработка ошибок в C++: от try/catch до RAII и noexcept

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

1. Базовый механизм: try, catch, throw

Исключение — это объект, который «выбрасывается» (throw) при возникновении ошибки. Программа может перехватить его (catch) и выполнить корректирующие действия, не прерывая работу полностью.

Синтаксис

#include <iostream>
#include <stdexcept>

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

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

Правила работы

  • throw — создаёт копию объекта и передаёт управление обработчику.
  • try — блок кода, где могут возникнуть исключения.
  • catch — перехватывает исключение по типу. Можно указать много блоков catch.
  • Если исключение не перехвачено, вызывается std::terminate().

Перехват по базовому классу

Все стандартные исключения наследуются от std::exception. Поэтому можно написать универсальный обработчик:

try {
    // код
} catch (const std::exception& e) {
    std::cerr << "Стандартная ошибка: " << e.what() << std::endl;
} catch (...) {
    std::cerr << "Неизвестное исключение" << std::endl;
}

Блок catch (...) ловит вообще все исключения, но без доступа к объекту.

2. Исключения в конструкторах и деструкторах

Конструкторы

Если конструктор выбрасывает исключение, объект считается не созданным, и деструктор для него не вызывается. Это важно для управления ресурсами. Пример:

class Resource {
public:
    Resource() {
        std::cout << "Ресурс выделен" << std::endl;
        throw std::runtime_error("Ошибка инициализации");
    }
    ~Resource() {
        std::cout << "Ресурс освобождён" << std::endl;
    }
};

int main() {
    try {
        Resource r;
    } catch (...) {
        std::cout << "Исключение перехвачено" << std::endl;
    }
    // Деструктор Resource НЕ будет вызван!
    return 0;
}

Деструкторы

Выбрасывать исключения из деструктора — плохая практика. Если деструктор вызывается во время раскрутки стека (stack unwinding), и он выбрасывает новое исключение, программа аварийно завершится. Всегда пишите деструкторы как noexcept (по умолчанию они такими и являются).

3. RAII — автоматическое управление ресурсами

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

Пример с умным указателем std::unique_ptr:

#include <memory>
#include <iostream>

void process() {
    auto ptr = std::make_unique<int>(42);
    // Если здесь возникнет исключение,
    // unique_ptr автоматически удалит память
    throw std::runtime_error("Ошибка в process");
    // delete не нужен!
}

int main() {
    try {
        process();
    } catch (...) {
        std::cout << "Память не утекла, спасибо RAII" << std::endl;
    }
    return 0;
}

Собственный RAII-класс

class FileGuard {
    FILE* file;
public:
    FileGuard(const char* name) {
        file = fopen(name, "r");
        if (!file) throw std::runtime_error("Не удалось открыть файл");
    }
    ~FileGuard() {
        if (file) fclose(file);
    }
    // Запрещаем копирование
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
};

void readFile() {
    FileGuard fg("data.txt");
    // Если исключение — файл закроется автоматически
}

4. Современные подходы: noexcept и альтернативы исключениям

Спецификатор noexcept

Начиная с C++11, вы можете пометить функцию как noexcept, гарантируя, что она не выбросит исключение. Это помогает компилятору оптимизировать код. Если noexcept-функция всё же выбрасывает исключение, программа завершается.

void safeFunction() noexcept {
    // Здесь нельзя бросать исключения
}

int compute(int x) noexcept {
    return x * 2;  // безопасно
}

Используйте noexcept для перемещающих конструкторов, деструкторов и простых функций.

Альтернативы: std::optional и std::variant

Иногда исключения излишни. Для функций, которые могут вернуть или не вернуть значение, используйте std::optional (C++17):

#include <optional>

std::optional<int> safeDivide(int a, int b) {
    if (b == 0) return std::nullopt;
    return a / b;
}

int main() {
    auto res = safeDivide(10, 0);
    if (res) {
        std::cout << *res << std::endl;
    } else {
        std::cout <<                

Блоги

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