Why doesn't C++ use std::nested_exception to allow throwing from destructor?

16.9k views Asked by At

The main problem with throwing exceptions from destructor is that in the moment when destructor is called another exception may be "in flight" (std::uncaught_exception() == true) and so it is not obvious what to do in that case. "Overwriting" the old exception with the new one would be the one of the possible ways to handle this situation. But it was decided that std::terminate (or another std::terminate_handler) must be called in such cases.

C++11 introduced nested exceptions feature via std::nested_exception class. This feature could be used to solve the problem described above. The old (uncaught) exception could be just nested into the new exception (or vice versa?) and then that nested exception could be thrown. But this idea was not used. std::terminate is still called in such situation in C++11 and C++14.

So the questions. Was the idea with nested exceptions considered? Are there any problems with it? Isn't the situation going to be changed in the C++17?

5

There are 5 answers

7
Nicol Bolas On BEST ANSWER

The problem you cite happens when your destructor is being executed as part of the stack unwinding process (when your object was not created as part of stack unwinding)1, and your destructor needs to emit an exception.

So how does that work? You have two exceptions in play. Exception X is the one that's causing the stack to unwind. Exception Y is the one that the destructor wants to throw. nested_exception can only hold one of them.

So maybe you have exception Y contain a nested_exception (or maybe just an exception_ptr). So... how do you deal with that at the catch site?

If you catch Y, and it happens to have some embedded X, how do you get it? Remember: exception_ptr is type-erased; aside from passing it around, the only thing you can do with it is rethrow it. So should people be doing this:

catch(Y &e)
{
  if(e.has_nested())
  {
    try
    {
      e.rethrow_nested();
    }
    catch(X &e2)
    {
    }
  }
}

I don't see a lot of people doing that. Especially since there would be an exceedingly large number of possible X-es.

1: Please do not use std::uncaught_exception() == true to detect this case. It is extremely flawed.

4
Cheers and hth. - Alf On

Nested exceptions just add most-likely-ignored information about what happened, which is this:

An exception X has been thrown, the stack is being unwound, i.e. destructors of local objects are being called with that exception “in flight”, and the destructor of one of those objects in turn throws an exception Y.

Ordinarily this means that cleanup failed.

And then this is not a failure that can be remedied by reporting it upwards and letting higher level code decide to e.g. use some alternative means to achieve its goal, because the object that held the information necessary to do the clean up has been destroyed, along with its information, but without doing its cleanup. So it's much like an assertion failing. The process state can be very ungood, breaking the assumptions of the code.

Destructors that throw can in principle be useful, e.g. as the idea Andrei once aired about indicating a failed transaction on exit from a block scope. That is, in normal code execution a local object that hasn't been informed of transaction success can throw from its destructor. This only becomes a problem when it clashes with C++'s rule for exception during stack unwinding, where it requires detection of whether the exception can be thrown, which appears to be impossible. Anyway then the destructor is being used just for its automatic call, not in its cleanup rôle. And so one can conclude that the current C++ rules assume the cleanup rôle for destructors.

10
Richard Hodges On

There is one use for std::nested exception, and only one use (as far as I have been able to discover).

Having said that, it's fantastic, I use nested exceptions in all my programs and as a result the time spent hunting obscure bugs is almost zero.

This is because nesting exceptions allow you to easily build a fully-annotated call stack which is generated at the point of the error, without any runtime overhead, no need for copious logging during a re-run (which will change the timing anyway), and without polluting program logic with error handling.

for example:

#include <iostream>
#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>

// this function will re-throw the current exception, nested inside a
// new one. If the std::current_exception is derived from logic_error, 
// this function will throw a logic_error. Otherwise it will throw a
// runtime_error
// The message of the exception will be composed of the arguments
// context and the variadic arguments args... which may be empty.
// The current exception will be nested inside the new one
// @pre context and args... must support ostream operator <<
template<class Context, class...Args>
void rethrow(Context&& context, Args&&... args)
{
    // build an error message
    std::ostringstream ss;
    ss << context;
    auto sep = " : ";
    using expand = int[];
    void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
    // figure out what kind of exception is active
    try {
        std::rethrow_exception(std::current_exception());
    }
    catch(const std::invalid_argument& e) {
        std::throw_with_nested(std::invalid_argument(ss.str()));
    }
    catch(const std::logic_error& e) {
        std::throw_with_nested(std::logic_error(ss.str()));
    }
    // etc - default to a runtime_error 
    catch(...) {
        std::throw_with_nested(std::runtime_error(ss.str()));
    }
}

// unwrap nested exceptions, printing each nested exception to 
// std::cerr
void print_exception (const std::exception& e, std::size_t depth = 0) {
    std::cerr << "exception: " << std::string(depth, ' ') << e.what() << '\n';
    try {
        std::rethrow_if_nested(e);
    } catch (const std::exception& nested) {
        print_exception(nested, depth + 1);
    }
}

void really_inner(std::size_t s)
try      // function try block
{
    if (s > 6) {
        throw std::invalid_argument("too long");
    }
}
catch(...) {
    rethrow(__func__);    // rethrow the current exception nested inside a diagnostic
}

void inner(const std::string& s)
try
{
    really_inner(s.size());

}
catch(...) {
    rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}

void outer(const std::string& s)
try
{
    auto cpy = s;
    cpy.append(s.begin(), s.end());
    inner(cpy);
}
catch(...)
{
    rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}


int main()
{
    try {
        // program...
        outer("xyz");
        outer("abcd");
    }
    catch(std::exception& e)
    {
        // ... why did my program fail really?
        print_exception(e);
    }

    return 0;
}

expected output:

exception: outer : abcd
exception:  inner : abcdabcd
exception:   really_inner
exception:    too long

Explanation of the expander line for @Xenial:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

args is a parameter pack. It represents 0 or more arguments (the zero is important).

What we're looking to do is to get the compiler to expand the argument pack for us while writing useful code around it.

Let's take it from outside in:

void(...) - means evaluate something and throw away the result - but do evaluate it.

expand{ ... };

Remembering that expand is a typedef for int[], this means let's evaluate an integer array.

0, (...)...;

means the first integer is zero - remember that in c++ it's illegal to define a zero-length array. What if args... represents 0 parameters? This 0 ensures that the array has at lease one integer in it.

(ss << sep << args), sep = ", ", 0);

uses the comma operator to evaluate a sequence of expressions in order, taking the result of the last one. The expressions are:

s << sep << args - print the separator followed by the current argument to the stream

sep = ", " - then make the separator point to a comma + space

0 - result in the value 0. This is the value that goes in the array.

(xxx params yyy)... - means do this once for each parameter in the parameter pack params

Therefore:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

means "for every parameter in params, print it to ss after printing the separator. Then update the separator (so that we have a different separator for the first one). Do all this as part of initialising an imaginary array which we will then throw away.

6
Wil Evers On

The real problem is that throwing from destructors is a logical fallacy. It's like defining operator+() to perform multiplication. Destructors should not be used as hooks for running arbitrary code. Their purpose is to deterministically release resources. By definition, that must not fail. Anything else breaks the assumptions needed to write generic code.

0
anton_rh On

The problem that may happen during stack unwinding with chaining exceptions from destructors is that the nested exception chain may be too long. For example, you have std::vector of 1 000 000 elements each of which throws an exception in its destructor. Let's assume the destructor of std::vector collects all exceptions from destructors of its elements into single chain of nested exceptions. Then resulting exception may be even bigger than original std::vector container. This may cause performance problems and even throwing std::bad_alloc during stack unwinding (that even couldn't be nested because there is not enough memory for doing that) or throwing std::bad_alloc in other unrelated places in the program.