Использование RAII для выделения исключений

Таким образом, путь для выделения исключений в С++ с помощью 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, который Джон Калб расширяет, поскольку "получение ответственности - это инициализация", - это гораздо более чистый способ борьбы с исключениями вместо использования явной попытки/блоки 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().

Ответ 1

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

Ответ 2

Это невозможно с RAII

Учитывая простое правило

Деструкторы никогда не должны бросать.

невозможно, чтобы RAII реализовала то, что вы хотите. Правило имеет одну простую причину: если деструктор выдает исключение во время разворачивания стека из-за исключения в полете, тогда вызывается terminate() и ваше приложение будет мертвым.

Альтернативный

В С++ 11 вы можете работать с lambdas, что может облегчить жизнь. Вы можете написать

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().

Там хорошая статья статьи об этой теме Herb Sutter, которая прекрасно объясняет это правило. Короче: если у вас есть функция f(), которая вызывается из деструктора во время разворачивания стека, выглядящего следующим образом

void f()
{
    RAII r;
    bla();
}

где деструктор RAII выглядит как

RAII::~RAII()
{
    if ( std::uncaught_exception() )
    {
        // ...
    }
    else
    {
        // ...
    }
}

тогда первая ветвь в деструкторе всегда будет выполняться, так как во внешнем деструкторе во время разматывания строк std::uncaught_exception() всегда будет возвращать true, даже внутри функций, вызванных из этого деструктора, включая деструктор RAII.