4

I have a class whose ctor makes a driver call, and whose dtor makes the matching terminating/release driver call. Those calls can fail. The problem is naturally with the dtor.

I am naturally aware of the common wisdom of avoiding exceptions in dtors, since, if you throw one during stack unwinding, you get std::terminate. But - I would rather not just "swallow" such errors and fail to report them - if I can. So, is it legitimate/idiomatic to write code saying:

~MyClass() noexcept(false) {
    auto result = something_which_may_fail_but_wont_throw();
    if (std::uncaught_exceptions() == 0) {
        throw some_exception(result);
    }
}

Or is this just baroque and not a good idea?

Note: This class does not have access to the standard output/error streams, nor a log etc.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/250074/discussion-on-question-by-einpoklum-should-i-use-stduncaught-exceptions-to). – sideshowbarker Dec 02 '22 at 03:23

2 Answers2

6

If the only thing you do is check whether uncaught_exceptions() is zero, then you can miss some cases where it's safe to propagate the exception. For example, consider

struct X {
    ~X() noexcept(false) {
        if (std::uncaught_exceptions() == 0) throw FooException{};
    }
};

struct Y {
    ~Y() {
        try {
            X x;
        } catch (const FooException&) {
            // handle exception
        }
    }
};

int main() {
    try {
        Y y;
        throw BarException{};
    } catch (const BarException&) {
        // handle 
    }
}

Here, y will be destroyed during stack unwinding. During Y's destructor, there is one uncaught exception in flight. The destructor creates an X object whose destructor subsequently has to decide whether to throw a FooException. It is safe for it to do so, because there will be an opportunity to catch the FooException before it gets to a point where std::terminate will be invoked. But X::~X determines that an uncaught exception is in flight, so it decides not to throw the exception.

There is nothing technically wrong with this, but it's potentially confusing that the behaviour of the try-catch block in Y::~Y depends on the context from which Y::~Y was invoked. Ideally, X::~X should still throw the exception in this scenario.

N4152 explains the correct way to use std::uncaught_exceptions:

A type that wants to know whether its destructor is being run to unwind this object can query uncaught_exceptions in its constructor and store the result, then query uncaught_exceptions again in its destructor; if the result is different, then this destructor is being invoked as part of stack unwinding due to a new exception that was thrown later than the object's construction.

In the above example, X::X() needs to store the value of std::uncaught_exceptions() during its construction, which will be 1. Then, the destructor will see that the value is still 1, which means that it's safe to let an exception escape from the destructor.

This technique should only be used in situations where you really need to throw from destructors and you are fine with the fact that whatever purpose will be served by throwing from the destructor will go unfulfilled if the std::uncaught_exceptions() check fails (forcing the destructor to either swallow the error condition or terminate the program). This rarely is the case.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Fair enough, but - that doesn't quite answer my question. I mean, yes, I will throw exceptions in less case than I might theoretically have been able to; and I could add an `uncaught_exceptions` field to maximize the potential for throwing. But either way - what do you think about the approach? – einpoklum Nov 29 '22 at 16:11
  • @einpoklum I wrote some thoughts about that in the last paragraph. – Brian Bi Nov 29 '22 at 23:22
-2

Under no circumstances should you throw from a c++ destructor.

From https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

18.5.1 The std::terminate() function [except.terminate] ... (1.4) — when the destruction of an object during stack unwinding (18.2) terminates by throwing an exception, or

So its not just 'wisdom' not to throw in a destructor. It will cause your program to crash/exit.

Instead, what I do for this situation, is have a method of the class called something like "Complete".

In the 'Complete' method, you can check for errors codes and throw safely.

You can add a data member (completed) - private - initialized to false, and set true in the Complete () method. And in your destructor, assert its true (so you can catch any cases where you forgot to call Complete).

Throwing from destructors will likely cause grave disorder in your program.

lewis
  • 1,116
  • 1
  • 12
  • 28
  • 2
    This quote doesn't apply here. If a destructor is called during stack unwinding, `std::uncaught_exceptions()` will not be zero and no exception will be thrown in OP's code. That's the point: make throwing conditional to avoid `std::terminate()` being called. – Evg Nov 29 '22 at 01:23
  • 1. My code will not cause the program to terminate. 2. A `complete()` method won't help, since the error I'm talking about only occurs during destruction, and after destruction, such a method will not be accessible anyway. – einpoklum Nov 29 '22 at 08:13
  • I don't follow why you believe you can throw from a destructor without triggering std::terminate. Perhaps there is some way you could avoid that. But there is certainly no point. As I said, you can use a complete method. That complete method can do anything (including shutting down subobjects) that your destructor would have done. For example - whatever direct objects you had - wrap them in optional or shared_ptr so they can be cleaned up before your owning object is destroyed. Or recursively apply the same trick - of a 'completed' method on those sub-objects. – lewis Nov 29 '22 at 14:56
  • @lewis: " don't follow why you believe you can throw from a destructor without triggering std::terminate" <- because if there are no other exceptions in flight, then std::terminate won't be triggered. "As I said, you can use a complete method" <- that defeats the whole purpose of having CADRe/RAII classes. – einpoklum Dec 13 '22 at 16:47
  • 1
    @einpoklum - first let me appologize. It should have been obvious from your question. But I had always thought destructors were noexcept(true). I didn't realize they could be noexcept(false), and so didn't adequately mentally process your code. MY BAD. SORRY. – lewis Dec 13 '22 at 18:32
  • @einpoklum Still I don't agree about your RAII point. It is QUITE USEFUL. Consider this example (I think pretty common - a database transaction - https://github.com/SophistSolutions/Stroika/blob/v2.1-Release/Library/Sources/Stroika/Foundation/Database/SQL/Transaction.h). This transaction does use RAII. But distinguishes failing case from success case based on calling Complete (which can throw). If this RIIA class ALWAYS treated destruction as completing the transaction, then an error would be misinterpretted as success. – lewis Dec 13 '22 at 18:33
  • @einpoklum I guess it MIGHT be useful to have the Transaction::DESTRUCTOR throw if rollback failed. But it would be just confusing it it sometimes threw on rollback failure, depending on whether there was an outstanding exception that triggered the original destruction - typically true). – lewis Dec 13 '22 at 18:36
  • @einpoklum this is an example of using the Transaction object - https://github.com/SophistSolutions/WhyTheFuckIsMyNetworkSoSlow/blob/v1-Release/Backend/Sources/Common/DB.inl#L32 - where you call Complete when you've succeeded, and the RIIA cleans up the incomplete transaction (rollback) if there was any failure during its processing. – lewis Dec 13 '22 at 18:50
  • @lewis: That example is not relevant, exactly because the underlying mechanism is transaction-based, meaning you can always destruct without doing anything which may fail. That is an atypical situation when interacting with external entities out of your program's control. – einpoklum Dec 13 '22 at 21:26
  • @einpoklum first, transaction rollback can fail (https://www.sqlite.org/lang_transaction.html#:~:text=If%20the%20transaction%20has%20already,might%20cause%20automatic%20transaction%20rollback.) at least with sqlite. Nothing todo about it, but ignore it. – lewis Dec 14 '22 at 22:13
  • @einpoklum second, and more interestingly, what do you mean by this example not being relevant because the underlying mechanism is transaction based? You are using RIIA so there are two cases - you completed successfully, and completed with a failure. You are trying to handle them all in the destructor, and detect success or failure cases based on std::uncaught_exceptions(); I'm just saying - dont handle the cleanup in destructor for 'worked' case, and ONLY handle the cleanup in the destructor for the 'failure' case. I think with both our approaches you have the same external constraints. – lewis Dec 14 '22 at 22:17
  • @einpoklum only for the 'works' case I can easily and safely raise an exception. And for the 'it failed' case, I cannot raise an exception (and you can). BUT - if this happened because of an exception, you cannot either. And if there wasn't an exception it didn't fail. – lewis Dec 14 '22 at 22:19
  • @einpoklum when you say 'external entities outside of your control' - I'm guessing you mean hardware state or network entities. In case of some failure (exception) - where you cannot contact those entities to cleanup, what can you do anyhow? You already have one excpetion (the original) being handled. What good would it do to throw a second exception saying you didn't cleanup properly? I suppose you COULD somehow 'merge' the two exceptions. MAYBE that is your point? If you had some 'merged exception' class, you could throw that from your noexcept(false) destructor? – lewis Dec 14 '22 at 22:23
  • @lewis: 1. The calling code needs to be notified of errors, to decide what they want to do with them. 2. Not every error is fatal; and some of them are even due to inopportune actions by the caller, which actually _should_ fail (but my classes don't know that). In these cases, there are other courses of action rather than "shut down the world". – einpoklum Dec 15 '22 at 18:00
  • @einpoklum OK - maybe I'm closer to understanding. And I believe the answer is you just cannot do this in c++. But I now see how a new C++ feature MIGHT allow you to accomplish what you want neatly (maybe ill thought out). – lewis Dec 16 '22 at 22:25
  • 1
    @einpoklum I think your complaint with the code you wrote is that you check if (std::uncaught_exceptions() == 0) you can throw more info; but you cannot handle the else case (am I right that you want to -and combine some NEW information in what is already being thrown?). Or are you already happy doing nothing in this case, and figuring that the what was originally thrown had all the error information you cared about? If you do NOT need to combine information, you are all set. – lewis Dec 16 '22 at 22:31
  • @einpoklum If you DO wish to combine the information of the currently in flight exception (which triggered this unwind/destruction) - then I think you need a new language feature that lets you capture the current exception (already easy) - but to replace it with a new one combining the two (some new exception class you can easily create which has an inner exception exception_ptr - like C# / .net does for example). Am I following you now? – lewis Dec 16 '22 at 22:36