Обработка ошибок в 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
Блоги
Рекомендаций по книгам
Читайте также