Использование RAII для вложения исключений
-
21-12-2019 - |
Вопрос
Итак, способ вложения исключений в 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 ++.
Решение
В отсутствие метода для перехвата (и использования) неперехваченного исключения в деструкторе невозможно повторно создать исключение, вложенное или нет, в контексте деструктора без std::terminate
вызывается (когда исключение генерируется в контексте обработки исключений).
std::current_exception
(в сочетании с std::rethrow_exception
) вернет только указатель на обрабатываемое в данный момент исключение.Это исключает его использование в данном сценарии, поскольку исключение в данном случае явно необработано.
Учитывая вышесказанное, единственный ответ, который можно дать, - это с эстетической точки зрения.Блоки try на функциональном уровне делают это немного менее уродливым.(настройте в соответствии с вашими предпочтениями в стиле):
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().
В режиме выпуска обычно не будет панели производительности по сравнению с способом выполнения try-catch, поскольку шаблоны встроены по умолчанию.
Еще одно интересное правило, которому нужно следовать:
Не используйте
std::uncaught_exception()
.
Там есть хороший статья об этом тема Херба Саттера, которая прекрасно объясняет это правило.Короче говоря:Если у вас есть функция f()
который называется изнутри деструктора во время разматывания стека выглядящий вот так
void f()
{
RAII r;
bla();
}
где деструктор RAII
выглядит как
RAII::~RAII()
{
if ( std::uncaught_exception() )
{
// ...
}
else
{
// ...
}
}
тогда всегда будет приниматься первая ветвь в деструкторе, так как во внешнем деструкторе во время разматывания стека std::uncaught_exception()
всегда будет возвращать true, даже внутри функций, вызываемых из этого деструктора, включая деструктор RAII
.