Обработка ошибок в 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 <<