Продвинутая обработка ошибок в 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 сообщает компилятору, что функция не бросает исключения. Это позволяет:
- Оптимизировать код (убирается код раскрутки стека).