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

5

26

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?

Splinter answered 14/5, 2016 at 13:32 Comment(14)
Well, main problem in throw in destructor is that the object won't be destroyed. And when object can't be properly destructed, it's bad.Rubric
Also, std::nested_exception requires that the nested exception object be copy-constructible. If a destructor is entered due to an thrown exception in progress, it is not guaranteed that the thrown exception is copy-constructible.Fancyfree
What would you do when you encounter an exception during unwinding? Continue unwinding? Unwind back in time to before you attempted to unwind?Cinelli
@Zereges, the object will be destroyed and all its subobjects too. Throwing exception from destructor is allowed, but not in the case when there is an active exception.Splinter
@melak47, yes, continue unwinding but with two active exceptions.Splinter
@Zereges: After doing some research, I found that anton is correct. Section 15.2, p2 clearly states that any object "whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects". So yes, if you throw in a destructor, subobjects will still be destroyed.Roark
@Sam Varshavchik It looks like it's illegal to throw non-copy-constructible exceptions stackoverflow.com/a/12826028 : "When the thrown object is a class object, the copy/move constructor and the destructor shall be accessible, even if the copy/move operation is elided (12.8)."Splinter
@anton_rh: That is the exact opposite of what the post you linked to said. A copy constructor only has to be accessible if you're copying the object. If you're moving the object, a move constructor must be accessible. So there is no language in the standard which requires that the type you throw be move-only. But there is language in the standard which requires that the type you throw be copyable if you try to use current_exception on it.Roark
@Nicol Bolas, I couldn't compile the code that throws non-copy-constructible object: coliru.stacked-crooked.com/a/82622616e148ad3d , though I didn't use current_exception.Splinter
Ultimately it's a judgment call but my view is, exceptions are supposed to be there to make your code easier to read. There are definitely cases where, using exceptions instead of C-style error handling will make your code much easier for most C++ programmers to read, because you are separating the "work" part from the "error handling" part, and all that. The point where you start throwing exceptions from destructors, using all this nested_exceptions, std::uncaught_exception() == true stuff, is a point where at least 50% of C++ programmers can no longer easily read your code. IMO.Interpretation
@anton_rh: It works fine for a type that is moveable: coliru.stacked-crooked.com/a/12b55771fdd6315c You forgot that deleting the copy constructor will also delete the move constructor unless you explicitly default it.Roark
@NicolBolas Thanks for clarification, but I probably wrongly expelled my thought. I meant, that from best practise point of view, throwing in destructor might look weird (Some concept might be better implemented in such case, that exception in destructor should not destroy that object. Moreover, you can't pretty much catch an exception in destructor. So from that point of view, throwing in destructor seems a bit weird for me.Rubric
@Zereges: "Moreover, you can't pretty much catch an exception in destructor." That's wrong; catching exceptions within a destructor is no different than elsewhere. I believe you meant that you can't catch the exception a destructor throws. Which is also wrong. Destructors of automatic variables are still called within the try block in which they are declared. And function-level try blocks exist.Roark
@Nicol Bolas, yes, I know. When I said it must be copy-constructible, I ment that it must be copy-constructible or move-constructible. Yes, copy/move-constructible are different things but they are similar (they make a new object for the existing object of the same type), so I just said must be copy-constructible. Sorry, if my comment was misleading. And my cite was "the copy /move constructor and the destructor shall be accessible"Splinter
R
11

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.

Roark answered 14/5, 2016 at 13:55 Comment(7)
... and is being replaced with std::uncaught_exceptions() in c++17, but not for this use case - it's to help with the development of transaction-like operations. (+1)Lotetgaronne
@RichardHodges: It is for this use case. It's specifically there to make it possible to detect if the object is being destroyed due to stack unwinding or if the object is being destroyed within the destructor of something that is being unwound. This is what allows it to help with transactions: to allow the destructor to understand why it was called.Roark
Re "how could a catch statement actually catch different kinds of nested_exceptions", this is not a problem. I don't like the standard library's nested exceptions but they're not completely unusable. Still the flawed argument against using them for throwing destructors, doesn't mean throwing destructors are meaningful in general. ;-)Sorehead
Right, but is there any other real-world motivating problem domain in which uncaught_exceptions is more useful than uncaught_exception? I am not aware of any.Lotetgaronne
@RichardHodges: There are no real-world applications for either function besides those. That's why uncaught_exception is being deprecated: because any use of it which is potentially legitimate is also broken.Roark
So the problem is how to handle multiple exceptions. If you handle Y, you miss handling X. If you handle X, you miss handling Y. Of course you can use constructions like if(e.has_nested()), but such code is difficult to maintain.Splinter
And I think handling X is more important than handling Y, because X is primary problem. So Y should be nested into X, not opposite. But here another problem occurs. If new exception Z is thrown where should it be nested? Should it be nested into X as the second nested exception (the current implementation of nested_exception doesn't allow this), or should it be nested into Y (so it forms X->Y->Z)? And also the current implementation of nested_exception may only nest X into Y and cannot do this in back order.Splinter
L
28

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.

Lotetgaronne answered 14/5, 2016 at 14:32 Comment(10)
"without polluting program logic with error handling" So all those try/catch blocks in your chain of functions is not "polluting" anything? Sorry, but I'd rather have "copious logging" than that nonsense. Even better would be to use an exception type that just stores the call-stack, obtained through platform-specific methods.Roark
@NicolBolas the use of function try blocks keeps the rethrow well out of the way of program logic (which will in reality be more than one line)... and a call stack is all very well, but not useful in an error log file where you may want to store additional state information (such as the contents of this). The catch block allows the collation of all that data with ease.Lotetgaronne
This is a very nice and instructive example of how to really use exceptions properly! Do you have any references where this method of nesting exceptions would be explained in more detail or an example of open source code doing this? Without comments the code is a bit hard to understand for a beginner.Bomarc
@Bomarc added some comments. I have used some 'advanced' techniques for template expansion, I apologise for that. Sadly this technique is under-used in library code, which is a shame as it provides a great deal of detail to users that need it. More information here: en.cppreference.com/w/cpp/error/throw_with_nestedLotetgaronne
One question: Is std::rethrow_exception(std::current_exception()); equivalent to throw; ?Puerile
Could I get some help understanding the line starting with void (expand{ .In particular, what the void ( is for, what the 0, ((ss << sep << args), sep = ", ", 0) is doing, or even what the ... is doing in this case.Cashandcarry
Many thanks for the time put into that explanation. I found myself since looking into en.cppreference.com/w/cpp/language/parameter_pack. Btw your last paragraph probably wants 'args' in place of 'params'.Cashandcarry
It does seem to mean a lot of exception handling code that doesn't actually handle exceptions. A few functions that throw could easily mean adding code to tens of other functions if you wanted to genuinely get that annotated call stack advantage. It's a lot of clutter to maintain right in the project code, for a debugging aid. I imagine there's definitely a place for it; I'm glad to have learnt it.Cashandcarry
@Cashandcarry an alternative could be to use boost::stack_trace at the throw site. Which way (if any) you want to go will depend on your use case and personal preferences.Lotetgaronne
If I am reading this correctly you only get the call stack between the catching and throwing frames, you don't get anything below the catching frame.Summersummerhouse
R
11

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.

Roark answered 14/5, 2016 at 13:55 Comment(7)
... and is being replaced with std::uncaught_exceptions() in c++17, but not for this use case - it's to help with the development of transaction-like operations. (+1)Lotetgaronne
@RichardHodges: It is for this use case. It's specifically there to make it possible to detect if the object is being destroyed due to stack unwinding or if the object is being destroyed within the destructor of something that is being unwound. This is what allows it to help with transactions: to allow the destructor to understand why it was called.Roark
Re "how could a catch statement actually catch different kinds of nested_exceptions", this is not a problem. I don't like the standard library's nested exceptions but they're not completely unusable. Still the flawed argument against using them for throwing destructors, doesn't mean throwing destructors are meaningful in general. ;-)Sorehead
Right, but is there any other real-world motivating problem domain in which uncaught_exceptions is more useful than uncaught_exception? I am not aware of any.Lotetgaronne
@RichardHodges: There are no real-world applications for either function besides those. That's why uncaught_exception is being deprecated: because any use of it which is potentially legitimate is also broken.Roark
So the problem is how to handle multiple exceptions. If you handle Y, you miss handling X. If you handle X, you miss handling Y. Of course you can use constructions like if(e.has_nested()), but such code is difficult to maintain.Splinter
And I think handling X is more important than handling Y, because X is primary problem. So Y should be nested into X, not opposite. But here another problem occurs. If new exception Z is thrown where should it be nested? Should it be nested into X as the second nested exception (the current implementation of nested_exception doesn't allow this), or should it be nested into Y (so it forms X->Y->Z)? And also the current implementation of nested_exception may only nest X into Y and cannot do this in back order.Splinter
S
2

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.

Sorehead answered 14/5, 2016 at 13:55 Comment(4)
Ok, suppose the object failed to close file or unlock mutex. Object is not destroyed and all its information is available. But how can you fix the described problems with higher level code? There is no much difference between failure of such pre-destructing methods and failure of destructor. All necessary information can be placed to exception itself.Splinter
@anton_rh: Not sure what you mean by "not destroyed", but the idea of placing deferred cleanup info in the the exception itself is one way to deal with things in some situations. That information can also be placed elsewhere. It requires some mechanism to have the deferred cleanup done, eventually, or to let other code using these resources know. In general it gets messy fast. It's a problem that the core language can't solve, and can only support when it becomes more clear what support is needed (that's still not clear, AFAIK).Sorehead
I don't mean to place deferred clean up info into exception. I mean, for example, if fstream::close failed to "clean up" resources, how can you handle this problem? Even if fstream object for which close was failed is still available (not destructed), you can't fix the problem using this object (for example by calling close second time). So I think there is no difference between throwing exception from fstream::close or fstream::~fstream. In both cases there is not much you can do.Splinter
@anton_rh: Well, when cleanup fails you can terminate. In the case of fstream::close the calling ordinary user's code is in position to do that. But in the case of a destructor failing during stack unwinding there is no calling ordinary user's code, so the C++ implementation does it.Sorehead
K
1

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.

Klarrisa answered 14/5, 2016 at 19:3 Comment(6)
By definition? Destructors sometimes fail to clean up their resources (fstream::close is an example). What should they do in this case? Terminating entire program may be not desired behavior. Wouldn't it be better to just notify the upper code about the problem and let it decide what to do?Splinter
There's no need to terminate the program when failing to close. fstream::close() is not a destructor; it's an ordinary member function. This is a good thing, because it allows the caller to say "close yourself, reporting errors as usual" instead of "I don't need you any more, go away".Klarrisa
The same I can say about destructors: there is no need to terminate the program when failing to close in destructor (currently destructors either silently ignore the problem or terminate entire program, the both behaviors aren't good). Reporting about a problem by throwing an exception from the destructor would be ok (it wouldn't be any worse than doing that from close), if there were no problem pending exceptions.Splinter
That problem only exists if you choose to break the paradigm by having a destructor do more than just cleanup, thus introducing an extra source of potential errors in a context where exceptions can't be used for reporting those errors. That implies you'll now have to come up with some other way to notify the user. Which is exactly what fstream::close() provided in the first place.Klarrisa
@WilEvers What is the "paradigm"? What is "just cleanup"? What do you do if "just cleanup" returns an error?Bainite
It is quite common and accepted for "lock guards" to release locks in their destructors. Furthermore, since C++ has no "finally" clause for try/catch blocks, I find using guard classes as replacements for "finally" is also acceptable and useful. However, you should NEVER throw from a dtor, and you should avoid doing anything that allocates memory.Summersummerhouse
S
1

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.

Splinter answered 15/5, 2016 at 3:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.