Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using RAII to nest exceptions

So the way to nest exceptions in C++ using std::nested_exception is:

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"));
  }
}

But this technique uses explicit try/catch blocks at every level where one wishes to nest exceptions, which is ugly to say the least.

RAII, which Jon Kalb expands as "responsibility acquisition is initialization", is a much cleaner way to deal with exceptions instead of using explicit try/catch blocks. With RAII, explicit try/catch blocks are largely only used to ultimately handle an exception, e.g. in order to display an error message to the user.

Looking at the above code, it seems to me that entering foo() can be viewed as entailing a responsibility to report any exceptions as std::runtime_error("foo failed") and nest the details inside a nested_exception. If we can use RAII to acquire this responsibility the code looks much cleaner:

void foo() {
  Throw_with_nested on_error("foo failed");

  // code that might throw
  std::ifstream file("nonexistent.file");
  file.exceptions(std::ios_base::failbit);
}

Is there any way to use RAII syntax here to replace explicit try/catch blocks?


To do this we need a type that, when its destructor is called, checks to see if the destructor call is due to an exception, nests that exception if so, and throws the new, nested exception so that unwinding continues normally. That might look like:

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));
    }
  }
};

However std::throw_with_nested() requires a 'currently handled exception' to be active, which means it doesn't work except inside the context of a catch block. So we'd need something like:

  ~Throw_with_nested() {
    if (std::uncaught_exception()) {
      try {
        rethrow_uncaught_exception();
      }
      catch(...) {
        std::throw_with_nested(std::runtime_error(msg));
      }
    }
  }

Unfortunately as far as I'm aware, there's nothing like rethrow_uncaught_excpetion() defined in C++.

like image 581
bames53 Avatar asked Nov 17 '13 21:11

bames53


2 Answers

In the absence of a method to catch (and consume) the uncaught exception in the destructor, there is no way to rethrow an exception, nested or not, in the context of the destructor without std::terminate being called (when the exception is thrown in the context of exception handling).

std::current_exception (combined with std::rethrow_exception) will only return a pointer to a currently handled exception. This precludes its use from this scenario as the exception in this case is explicitly unhandled.

Given the above, the only answer to give is from an aesthetic perspective. Function level try blocks make this look slightly less ugly. (adjust for your style preference):

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"));
}
like image 163
mythagel Avatar answered Sep 22 '22 09:09

mythagel


It's impossible with RAII

Considering the simple rule

Destructors must never throw.

it is impossible with RAII to implement the thing you want. The rule has one simple reason: If a destructor throws an exception during stack unwinding due to an exception in flight, then terminate() is called and your application will be dead.

An alternative

In C++11 you can work with lambdas which can make life a little easier. You can write

void foo()
{
    giveErrorContextOnFailure( "foo failed", [&]
    {
        // code that might throw
        std::ifstream file("nonexistent.file");
        file.exceptions(std::ios_base::failbit);
    } );
}

if you implement the function giveErrorContextOnFailure in the following way:

template <typename F>
auto giveErrorContextOnFailure( const char * msg, F && f ) -> decltype(f())
{
    try { return f(); }
    catch { std::throw_with_nested(std::runtime_error(msg)); }
}

This has several advantages:

  • You encapsulate how the error is nested.
  • Changing the way errors are nested can be changed for the whole program, if this technique is followed strictly program wide.
  • The error message can be written before the code just as in RAII. This technique can be used for nested scopes as well.
  • There's less code repetition: You don't have to write try, catch, std::throw_with_nested and std::runtime_error. This makes your code more easily maintainable. If you want to change the behavior of your program you need to change your code in one place only.
  • The return type will be deduced automatically. So if your function foo() should return something, then you just add return before giveErrorContextOnFailure in your function foo().

In release mode there will typically be no performance panelty compared to the try-catch-way of doing things, since templates are inlined by default.

One more interesting rule to follow:

Do not use std::uncaught_exception().

There's a nice article about this topic by Herb Sutter which explains this rule perfectly. In short: If you have a function f() which is called from within a destructor during stack unwinding looking like this

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

where the destructor of RAII looks like

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

then the first branch in the destructor will always be taken, since in the outer destructor during stack unwinding std::uncaught_exception() will always return true, even inside functions called from that destructor including the destructor of RAII.

like image 44
Ralph Tandetzky Avatar answered Sep 19 '22 09:09

Ralph Tandetzky