Использование RAII для вложения исключений
Таким образом, способ вложения исключений в C++ с использованием std::nested_exception
является:
void foo() {
try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
}
Но этот метод использует явные блоки try/catch на каждом уровне, где каждый хочет вкладывать исключения, что по меньшей мере уродливо.
RAII, который Джон Калб расшифровывает как "получение ответственности - это инициализация", является гораздо более чистым способом обработки исключений, вместо использования явных блоков try/catch. В RAII явные блоки try/catch в основном используются только для окончательной обработки исключения, например, для отображения пользователю сообщения об ошибке.
Глядя на приведенный выше код, мне кажется, что ввод foo()
можно рассматривать как влечет за собой ответственность сообщать о любых исключениях, как std::runtime_error("foo failed")
и вложите детали в исключение nested_exception. Если мы можем использовать RAII, чтобы получить эту ответственность, код выглядит намного чище:
void foo() {
Throw_with_nested on_error("foo failed");
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
Есть ли способ использовать синтаксис RAII здесь, чтобы заменить явные блоки try/catch?
Чтобы сделать это, нам нужен тип, который при вызове деструктора проверяет, вызван ли вызов деструктора исключением, вкладывает это исключение, если это так, и выдает новое, вложенное исключение, чтобы разматывание продолжалось нормально. Это может выглядеть так:
struct Throw_with_nested {
const char *msg;
Throw_with_nested(const char *error_message) : msg(error_message) {}
~Throw_with_nested() {
if (std::uncaught_exception()) {
std::throw_with_nested(std::runtime_error(msg));
}
}
};
тем не мение std::throw_with_nested()
требует, чтобы "текущее обработанное исключение" было активным, что означает, что оно не работает, кроме как в контексте блока catch. Так что нам нужно что-то вроде:
~Throw_with_nested() {
if (std::uncaught_exception()) {
try {
rethrow_uncaught_exception();
}
catch(...) {
std::throw_with_nested(std::runtime_error(msg));
}
}
}
К сожалению, насколько я знаю, нет ничего подобного rethrow_uncaught_excpetion()
определено в C++.
2 ответа
В отсутствие метода для перехвата (и использования) необработанного исключения в деструкторе, невозможно перебросить исключение, вложенное или нет, в контексте деструктора без std::terminate
вызывается (когда исключение выдается в контексте обработки исключения).
std::current_exception
(в сочетании с std::rethrow_exception
) будет возвращать только указатель на обработанное в настоящий момент исключение. Это исключает его использование из этого сценария, поскольку исключение в этом случае явно не обрабатывается.
Учитывая вышесказанное, единственный ответ дать с эстетической точки зрения. Блоки пробного уровня функций делают этот вид немного менее уродливым. (отрегулируйте в соответствии со своими предпочтениями стиля):
void foo() try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
Это невозможно с RAII
Учитывая простое правило
Деструкторы никогда не должны бросать.
с RAII невозможно реализовать то, что вы хотите. Правило имеет одну простую причину: если деструктор выдает исключение во время разматывания стека из-за исключения в полете, то terminate()
называется, и ваше приложение будет мертвым.
Альтернатива
В C++11 вы можете работать с лямбдами, которые могут немного облегчить жизнь. Ты можешь написать
void foo()
{
giveErrorContextOnFailure( "foo failed", [&]
{
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
} );
}
если вы реализуете функцию giveErrorContextOnFailure
следующим образом:
template <typename F>
auto giveErrorContextOnFailure( const char * msg, F && f ) -> decltype(f())
{
try { return f(); }
catch { std::throw_with_nested(std::runtime_error(msg)); }
}
Это имеет несколько преимуществ:
- Вы инкапсулируете, как ошибка вложена.
- Изменение способа вложения ошибок может быть изменено для всей программы, если эта методика строго соблюдается во всей программе.
- Сообщение об ошибке может быть написано перед кодом так же, как в RAII. Эту технику можно использовать и для вложенных областей.
- Там меньше повторения кода: вам не нужно писать
try
,catch
,std::throw_with_nested
а такжеstd::runtime_error
, Это делает ваш код более простым в обслуживании. Если вы хотите изменить поведение вашей программы, вам нужно изменить код только в одном месте. - Тип возврата будет выведен автоматически. Так что если ваша функция
foo()
должен что-то вернуть, тогда просто добавьreturn
доgiveErrorContextOnFailure
в вашей функции foo().
В режиме релиза, как правило, производительность не сравнивается с попыткой сделать что-то, так как шаблоны встроены по умолчанию.
Еще одно интересное правило для подражания:
Не использовать
std::uncaught_exception()
,
Есть хорошая статья Херба Саттера на эту тему, которая прекрасно объясняет это правило. Короче говоря: если у вас есть функция f()
который вызывается из деструктора во время разматывания стека, выглядит так
void f()
{
RAII r;
bla();
}
где деструктор RAII
похоже
RAII::~RAII()
{
if ( std::uncaught_exception() )
{
// ...
}
else
{
// ...
}
}
тогда первая ветвь в деструкторе всегда будет занята, так как во внешнем деструкторе при разматывании стека std::uncaught_exception()
всегда будет возвращать true, даже внутри функций, вызываемых из этого деструктора, включая деструктор RAII
,