Использование 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 ++.

12

Решение

В отсутствие метода для перехвата (и использования) необработанного исключения в деструкторе, невозможно перебросить исключение, вложенное или нет, в контексте деструктора без 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"));
}
3

Другие решения

Это невозможно с 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,

3

По вопросам рекламы [email protected]