Using RAII to nest exceptions
Asked Answered
C

2

12

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++.

Coextensive answered 17/11, 2013 at 21:0 Comment(1)
What you're trying to do really doesn't have much to do with RAII. Sure, you can say that anything is "entering a responsibility to do X". main is entering a responsibility to execute the program. sqrt is entering a responsibility to compute the square root of something. But at its root, RAII is about tying certain behavior to whenever an object goes out of scope, regardless of why or how it goes out of scope. The entire point in RAII is that we don't need to know why an object is being destroyed (whether an exception was thrown), we just know it is being destroyed.Cabbageworm
U
3

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"));
}
Unattached answered 17/11, 2013 at 23:45 Comment(2)
I don't think your claim "std::current_exception (combined with std::rethrow_exception) will only return a pointer to a currently handled exception" is accurate. current_exception returns a shared pointer (exception_ptr) to a copy of the pending exception. By taking a reference, you ensure it stays around until the last reference dies.Mablemabry
@Mablemabry Perhaps I have misunderstood you or was unclear above. ideone.com/nlsLnOUnattached
A
3

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.

Algoid answered 18/11, 2013 at 10:31 Comment(3)
The rule about not throwing from destructors isn't actually a hard and fast rule of C++, it's simply what's been found experience to be best practice. If there were a way to get the RAII type to work as I want throwing would be perfectly safe, because it would only ever replace an exception that's already being thrown, and never just throw a second exception.Coextensive
Point taken about uncaught_exception() though. I'm familiar with Herb's article but I hadn't thought carefully about it in my usage scenario; It's true that catching exceptions this way would mean every lower level would end up nesting exceptions, adding garbage to the context I want to preserve.Coextensive
However Herb has proposed new mechanisms which allow a destructor to detect if it's being called directly as a result of unwinding for an exception, or if it's being called for normal destruction. E.g. see Herb's paper n3614. Replacing uncaught_exceptions() with such a mechanism would fix this issue.Coextensive

© 2022 - 2024 — McMap. All rights reserved.