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

Другие вопросы по тегам