C++ using RAII with destructor that throws
Asked Answered
C

2

11

Let say I have RAII class:

class Raii {
    Raii() {};
    ~Raii() { 
        if (<something>) throw std::exception();
    }
};

And if I have the function:

void foo() {
    Raii raii;    

    if (something) {
       throw std::exception();
    }
} 

This is bad because while cleaning for the first exception we can throw again and this will terminate the process.

My question is - What is a good pattern to use raii for code that the cleanup might throw?

For example is this good or bad - why?

class Raii {
    Raii() {};
    ~Raii() {
        try {
           if (<something>) throw std::exception();
        }
        catch (...) {
           if (!std::uncaught_exception())
               throw;
        }
    }
};

Note that Raii object is always stack-allocated object - and this is not the general throw from destructor problem.

Crackup answered 2/4, 2016 at 1:4 Comment(2)
This isn't really any different from the more general case of "how do I prevent termination if I have a class whose destructor can throw". I don't think there is a good answer other than "destructors should not throw".Partial
Without the destructor doing some work there is no RAII. I really hope that there is something that could be done. Say something based on std::uncaught_exception where you can detect if there is exception in progress for example.Crackup
E
10

C++ will almost certainly have a function to obtain the current exception count as of C++1z (aka C++17 if they publish it on time!): std::uncaught_exceptions (note the plural "s"). Also, destructors are declared as noexcept by default (meaning that if you attempt to exit a destructor via an exception, std::terminate is called).

So, first, mark your destructor as throwing (noexcept(false)). Next, track the number of active exceptions in ctor, compare it to the value in dtor: if there are more uncaught exceptions in the dtor, you know that you are currently in the process of stack unwinding, and throwing again will result in a call to std::terminate.

Now you decide exactly how exceptional you really are and how you wish to handle the situation: terminate the program, or just swallow the inner exception?

A poor imitation is to not throw if uncaught_exception (singular) returns true, but that makes the exceptions not work when called from a different dtor's triggered by unrolling that is trying to catch and process your exception. This option is available in current C++ standards.

Emilio answered 2/4, 2016 at 1:30 Comment(2)
@kerrek has C++17 been finalized?Emilio
The relevant paper has been voted in and applied (N4582), and there's virtually no chance it'll be taken out, now that the WD is considered "feature-complete" for 17.Comptom
Z
5

The advice from the ScopeGuard article was

In the realm of exceptions, it is fundamental that you can do nothing if your "undo/recover" action fails. You attempt an undo operation, and you move on regardless whether the undo operation succeeds or not.

It may sound crazy, but consider:

  1. I manage to run out of memory and get a std::bad_alloc exception
  2. My clean-up code logs the error
  3. Unfortunately, the write fails (maybe the disk is full), and tries to throw an exception

Can I undo the log write? Should I try?

When an exception is thrown, all you really know is that the program is in an invalid state. You shouldn't be surprised that some impossible things turn out to be possible after all. Personally, I've seen far more cases where Alexandrescu's advice makes the most sense than otherwise: try to clean up, but recognize that the first exception means that things are already in an invalid state, so additional failures -- especially failures caused by the first problem ("error cascade") -- should not be a surprise. And trying to handle them is not going to end well.


I should probably mention that Cap'n Proto does exactly what you've proposed:

When Cap’n Proto code might throw an exception from a destructor, it first checks std::uncaught_exception() to ensure that this is safe. If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel.

But, as Yakk said, destructors became nothrow(true) by default in C++11. Which means that if you want to do this, you need to be sure that in C++11 and later you mark the destructor as nothrow(false). Otherwise, throwing an exception from the destructor will terminate the program even if there is no other exception in flight. And note, "If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel."

Zane answered 2/4, 2016 at 2:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.