RAII vs. exceptions
Asked Answered
H

8

53

The more we use RAII in C++, the more we find ourselves with destructors that do non-trivial deallocation. Now, deallocation (finalization, however you want to call it) can fail, in which case exceptions are really the only way to let anybody upstairs know of our deallocation problem. But then again, throwing-destructors are a bad idea because of the possibility of exceptions being thrown during stack unwinding. std::uncaught_exception() lets you know when that happens, but not much more, so aside from letting you log a message before termination there's not much you can do, unless you're willing to leave your program in an undefined state, where some stuff is deallocated/finalized and some not.

One approach is to have no-throw destructors. But in many cases that just hides a real error. Our destructor might, for example, be closing some RAII-managed DB connections as a result of some exception being thrown, and those DB connections might fail to close. This doesn't necessarily mean we're ok with the program terminating at this point. On the other hand, logging and tracing these errors isn't really a solution for every case; otherwise we would have had no need for exceptions to begin with. With no-throw destructors we also find ourselves having to create "reset()" functions that are supposed to be called before destruction - but that just defeats the whole purpose of RAII.

Another approach is just to let the program terminate, as it's the most predictable thing you can do.

Some people suggest chaining exceptions, so that more than one error can be handled at a time. But I honestly never actually seen that done in C++ and I've no idea how to implement such a thing.

So it's either RAII or exceptions. Isn't it? I'm leaning toward no-throw destructors; mainly because it keeps things simple(r). But I really hope there's a better solution, because, as I said, the more we use RAII, the more we find ourselves using dtors that do non-trivial things.

Appendix

I'm adding links to interesting on-topic articles and discussions I've found:

Harville answered 1/10, 2008 at 19:14 Comment(1)
perhaps just register a callback function for your class to call with any error that occurs during destruction? Coming from JavaScript I'm surprised to see how rarely I see callbacks used to solve problems like this in C++.Univalent
B
17

You SHOULD NOT throw an exception out of a destructor.

Note: Updated to refeclt changes in the standard:

In C++03
If an exception is already propagating then the application will terminate.

In C++11
If the destructor is noexcept (the default) then the application will terminate.

The Following is based on C++11

If an exception escapes a noexcept function it is implementation defined if the stack is even unwound.

The Following is based on C++03

By terminate I mean stop immediately. Stack unwinding stops. No more destructors are called. All bad stuff. See the discussion here.

throwing exceptions out of a destructor

I don't follow (as in disagree with) your logic that this causes the destructor to get more complicated.
With the correct usage of smart pointers this actually makes the destructor simpler as everything now becomes automatic. Each class tides up its own little piece of the puzzle. No brain surgery or rocket science here. Another Big win for RAII.

As for the possibility of std::uncaught_exception() I point you at Herb Sutters article about why it does not work

Brandnew answered 1/10, 2008 at 19:19 Comment(13)
I never said the possibility of dense faults causes dtors to be more complicated. RAII causes that. The more you use dtors to tear-down stuff, the more likely you are to encounter errors during tear-down.Harville
I do agree with Assaf. You put more (cleanup) stuff in dtors so it will be executed when exceptions are thrown, but the degenerate case of this is a function with only ctors and (implicit) dtors. So a lot of stuff in dtors -> is opportunity for a lot of exceptions.Juice
Sorry 'QBziZ' I am unable to follow your logic. Why don't you post an answer with the appropriate detail that explains your position so it can be voted up or down appropriately.Brandnew
@Juice I agree with Martin: What you are saying is quite similar to "the more code we add in a program, the more risks we're taking something will fail". Like any cleanup funct, the destructor must TRY to clean. But unlike other functs, it can't throw because a destructor is NOT like others functs.Likelihood
This is essentially saying "You cannot use RAII for anything complex" - not with other libraries with API which doesn't guarantee success; not with device I/O; etc.Sultan
@einpoklum: I believe that RAII is one of the most useful (and powerful) tools in C++ and by using it correctly you can simplify a lot of code. I don't understand how you got to that conclusion "You cannot use RAII for anything complex", if this is the conclusion you get from reading what I say then I need to make some edits to be more clear because this is the opposite of what I believe.Brandnew
@MartinYork: Suppose I have a resource which is released by calling status_type release_foo(foo_handle_type handle). In general, we cannot assume that the return value can just be ignored (i.e. this is isn't like a dropped connection). By your guideline, we cannot have a RAII class for "foo" resources. Am I misunderstanding?Sultan
@einpoklum: Does this help. If you shouldn't throw exceptions in a destructor, how do you handle errors in it?Brandnew
@MartinYork: It helps suggest you are indeed telling us not to use RAII for resource management if resource release is not guaranteed to succeed. i.e. not use RAII in complex cases.Sultan
@Sultan I disagree. Use RAII for all cases. But be aware that users of your code may need additional functionality if their use case is more complex, and they need extra manual steps.Brandnew
How will I use RAII in such cases if I follow your suggestion not to throw exceptions, then?Sultan
@Sultan You (the resource author) can't do anything. What you do in this case will always depend on the context that you (the resource author) do not have. You have to provide functionality for the user of the resource (who has context) to either do manual checks and perform the correct actions (or ignore the potential situation).Brandnew
This is the design of the std::fstream by default you don't do anything when the file fails to close in the destructor (any exceptions are caught and discarded in the destructor). The resource class ignores these errors. But the resource provides functionality for the user of the resource to be able to check and take action if that is appropriate/possible. i.e. In a user application, you can call close() and check the state and show an error dialogue (allowing corrective action).Brandnew
L
8

From the original question:

Now, deallocation (finalization, however you want to call it) can fail, in which case exceptions are really the only way to let anybody upstairs know of our deallocation problem

Failure to cleanup a resource either indicates:

  1. Programmer error, in which case, you should log the failure, followed by notifying the user or terminating the application, depending on application scenario. For example, freeing an allocation that has already been freed.

  2. Allocator bug or design flaw. Consult the documentation. Chances are the error is probably there to help diagnose programmer errors. See item 1 above.

  3. Otherwise unrecoverable adverse condition that can be continued.

For example, the C++ free store has a no-fail operator delete. Other APIs (such as Win32) provide error codes, but will only fail due to programmer error or hardware fault, with errors indicating conditions like heap corruption, or double free, etc.

As for unrecoverable adverse conditions, take the DB connection. If closing the connection failed because the connection was dropped -- cool, you're done. Don't throw! A dropped connection (should) result in a closed connection, so there's no need to do anything else. If anything, log a trace message to help diagnose usage issues. Example:

class DBCon{
public:
  DBCon() { 
    handle = fooOpenDBConnection();
  }
  ~DBCon() {
    int err = fooCloseDBConnection();
    if(err){
      if(err == E_fooConnectionDropped){
        // do nothing.  must have timed out
      } else if(fooIsCriticalError(err)){
        // critical errors aren't recoverable.  log, save 
        //  restart information, and die
        std::clog << "critical DB error: " << err << "\n";
        save_recovery_information();
        std::terminate();
      } else {
        // log, in case we need to gather this info in the future,
        //  but continue normally.
        std::clog << "non-critical DB error: " << err << "\n";
      }
    }
    // done!
  }
};

None of these conditions justify attempting a second kind of unwind. Either the program can continue normally (including exception unwind, if unwind is in progress), or it dies here and now.

Edit-Add

If you really want to be able to keep some sort of link to those DB connections that can't close -- perhaps they failed to close due to intermittent conditions, and you'd like to retry later -- then you can always defer cleanup:

vector<DBHandle> to_be_closed_later;  // startup reserves space

DBCon::~DBCon(){
  int err = fooCloseDBConnection();
  if(err){
    ..
    else if( fooIsRetryableError(err) ){
      try{
        to_be_closed.push_back(handle);
      } catch (const bad_alloc&){
        std::clog << "could not close connection, err " << err << "\n"
      }
    }
  }
}

Very not pretty, but it might get the job done for you.

Lampert answered 1/10, 2008 at 22:9 Comment(10)
Your style assumes your object knows what is best for the application by using terminate(). This is hardly ever (read never) the case, it is the surrounding code (ie the controlling code) that has the context to decide what to do with errors.Brandnew
You seem to be suggesting that unless it's a bug in code or design, it's recoverable, as in your DB example; but there are examples in which a non-recoverable error may be encountered during RAII unwinding.Harville
Unrecoverable for your object, but your object should not stop the application. You must cede that responsibility to a part of the application that has more context. e.g. Other resources may still need cleaning up properly. Unilateral termination is not acceptable in the majority of situations.Brandnew
@Assaf - exception handling doesn't support this sort of recovery. Consider the case of two DB objects being unwound, and both 'fail' to cleanup. If an application needs to know about such failures, it should invoke a 'close()' member on the object before destruction.Lampert
@Martin - agreed, termination isn't the only answer, nor is it the typical answer. 'do nothing' or 'redesign' are the common answers. Give the object a throwing 'close()' member if you need to be informed of the failure. dtors should clean up and be silent.Lampert
There are many situations in which the correct behavior following a cleanup failure is neither "continue as though nothing is wrong" nor "kill the whole system instantly". For example, a "save document" method should throw an exception unless everything works normally including the close. If saving a document requires writing two files simultaneously and a USB drive is unplugged during a SaveDocument method, and a write to one file throws an exception (likely); the destructor for the other file will also fail. Having the application instantly terminate would be very rude, but...Flowerdeluce
...ignoring a failure to close a file shouldn't be considered acceptable either. Correct behavior would be for the exceptions to unwind to a point where a user could be informed about what did or did not work, and take action accordingly.Flowerdeluce
@Flowerdeluce - unfortunately, you can't do this. An existing exception may already be unwinding, and an exception leaving a destructor colliding with the existing exception results in a forced call to std::terminate(). At best, you could add a function to check for errors, and instruct users to call it "if they want to" before automatic destruction takes place.Lampert
@Aaron: It's too bad that neither C#, nor Java, nor .net, provides a convenient method for a cleanup method to know whether it's being called under "normal" circumstances or because of exception unwinding. Perhaps the best bet is for the cleanup routine to have some method it can invoke to say something's wrong and the UI should let someone know about it, but that's not exactly elegant.Flowerdeluce
@Aaron: Right, that's what the question is for. There's an issue when finalizing might involve commiting, which can fail, but occurs in destructors which can't throw.Billboard
G
7

You're looking at two things:

  1. RAII, which guarantees that resources are cleaned up when scope is exited.
  2. Completing an operation and finding out whether it succeeded or not.

RAII promises that it will complete the operation (free memory, close the file having attempted to flush it, end a transaction having attempted to commit it). But because it happens automatically, without the programmer having to do anything, it doesn't tell the programmer whether those operations it "attempted" succeeded or not.

Exceptions are one way to report that something failed, but as you say, there's a limitation of the C++ language that means they aren't suitable to do that from a destructor[*]. Return values are another way, but it's even more obvious that destructors can't use those either.

So, if you want to know whether your data was written to disk, you can't use RAII for that. It does not "defeat the whole purpose of RAII", since RAII will still try to write it, and it will still release the resources associated with the file handle (DB transaction, whatever). It does limit what RAII can do -- it won't tell you whether the data was written or not, so for that you need a close() function that can return a value and/or throw an exception.

[*] It's quite a natural limitation too, present in other languages. If you think RAII destructors should throw exceptions to say "something has gone wrong!", then something has to happen when there's already an exception in flight, that is "something else has gone wrong even before that!". Languages that I know that use exceptions don't permit two exceptions in flight at once - the language and syntax simply don't allow for it. If RAII is to do what you want, then exceptions themselves need to be redefined so that it makes sense for one thread to have more than one thing going wrong at a time, and for two exceptions to propagate outward and two handlers to be called, one to handle each.

Other languages allow the second exception to obscure the first, for example if a finally block throws in Java. C++ pretty much says that the second one must be suppressed, otherwise terminate is called (suppressing both, in a sense). In neither case are the higher stack levels informed of both faults. What is a bit unfortunate is that in C++ you can't reliably tell whether one more exception is one too many (uncaught_exception doesn't tell you that, it tells you something different), so you can't even throw in the case where there isn't already an exception in flight. But even if you could do it in that case, you'd still be stuffed in the case where one more is one too many.

Gravy answered 1/10, 2008 at 19:14 Comment(0)
L
6

It reminds me a question from a colleague when I explained him the exception/RAII concepts: "Hey, what exception can I throw if the computer's switched off?"

Anyway, I agree with Martin York's answer RAII vs. exceptions

What's the deal with Exceptions and Destructors?

A lot of C++ features depend on non-throwing destructors.

In fact, the whole concept of RAII and its cooperation with code branching (returns, throws, etc.) is based on the fact deallocation won't fail. In the same way some functions are not supposed to fail (like std::swap) when you want to offer high exception guarantees to your objects.

Not that it doesn't mean you can't throw exceptions through destructors. Just that the language won't even try to support this behaviour.

What would happen if it was authorized?

Just for the fun, I tried to imagine it...

In the case your destructor fails to free your resource, what will you do? Your object is probably half destructed, what would you do from an "outside" catch with that info? Try again? (if yes, then why not trying again from within the destructor?...)

That is, if you could access your half-destructed object it anyway: What if your object is on the stack (which is the basic way RAII works)? How can you access an object outside its scope?

Sending the resource inside the exception?

Your only hope would be to send the "handle" of the resource inside the exception and hoping code in the catch, well... try again to deallocate it (see above)?

Now, imagine something funny:

 void doSomething()
 {
    try
    {
       MyResource A, B, C, D, E ;

       // do something with A, B, C, D and E

       // Now we quit the scope...
       // destruction of E, then D, then C, then B and then A
    }
    catch(const MyResourceException & e)
    {
       // Do something with the exception...
    }
 }

Now, let's imagine for some reason the destructor of D fails to deallocate the resource. You coded it to send an exception, that will be caught by the catch. Everything goes well: You can handle the failure the way you want (how you will in a constructive way still eludes me, but then, it is not the problem now).

But...

Sending the MULTIPLE resources inside the MULTIPLE exceptions?

Now, if ~D can fail, then ~C can, too. as well as ~B and ~A.

With this simple example, you have 4 destructors which failed at the "same moment" (quitting the scope). What you need is not not a catch with one exception, but a catch with an array of exceptions (let's hope the code generated for this does not... er... throw).

    catch(const std::vector<MyResourceException> & e)
    {
       // Do something with the vector of exceptions...
       // Let's hope if was not caused by an out-of-memory problem
    }

Let's get retarted (I like this music...): Each exception thrown is a different one (because the cause is different: Remember that in C++, exceptions need not derive from std::exception). Now, you need to simultaneously handle four exceptions. How could you write catch clauses handling the four exceptions by their types, and by the order they were thrown?

And what if you have multiple exceptions of the same type, thrown by multiple failed deallocation? And what if when allocating the memory of the exception arrays of arrays, your program goes out of memory and, er... throw an out of memory exception?

Are you sure you want to spend time on this kind of problem instead of spending it figuring why the deallocation failed or how to react to it in another way?

Apprently, the C++ designers did not see a viable solution, and just cut their losses there.

The problem is not RAII vs Exceptions...

No, the problem is that sometimes, things can fail so much that nothing can be done.

RAII works well with Exceptions, as long as some conditions are met. Among them: The destructors won't throw. What you are seeing as an opposition is just a corner case of a single pattern combining two "names": Exception and RAII

In the case a problem happens in the destructor, we must accept defeat, and salvage what can be salvaged: "The DB connection failed to be deallocated? Sorry. Let's at least avoid this memory leak and close this File."

While the exception pattern is (supposed to be) the main error handling in C++, it is not the only one. You should handle exceptional (pun intended) cases when C++ exceptions are not a solution, by using other error/log mechanisms.

Because you just met a wall in the language, a wall no other language that I know of or heard of went through correctly without bringing down the house (C# attempt was worthy one, while Java's one is still a joke that hurts me on the side... I won't even speak about scripting languages who will fail on the same problem in the same silent way).

But in the end, no matter how much code you'll write, you won't be protected by the user switching the computer off.

The best you can do, you already wrote it. My own preference goes with a throwing finalize method, a non-throwing destructor cleaning resources not finalized manually, and the log/messagebox (if possible) to alert about the failure in the destructor.

Perhaps you're not putting up the right duel. Instead of "RAII vs. Exception", it should be "Trying to freeing resources vs. Resources that absolutely don't want to be freed, even when threatened by destruction"

:-)

Likelihood answered 16/10, 2008 at 21:51 Comment(3)
The fundamental reason C++ cannot support the behavior is that there is no common base type for exceptions. If there were a common base type which included properties to get pointers for child and sibling, along with methods for should_catch<T> and is_resolved, handling multiple exceptions should be quite workable. A catch Foo should be run if any pending exception satisfies Foo; after the catch, if any non-resolved exceptions remain, the system should continue unwinding.Flowerdeluce
But the destructor can't be the place where defeat is accepted. It need to be up to the user code. The destructor typically isn't able to make app-level considerations, or even decide to log anything.Sultan
@Sultan : In these cases, you can have a close() method that should be idempotent, and that could be called manually if someone really wants to do something about it. The destructor then can call close() if still needed, and catch the error if any. . . But this needs serious reflection on how you class works (e.g. can it be used once the close() method is called, and if it can't but still is, what happens?). . . But in my experience, most classes do not need this pattern, because when you are cleaning up your things, you don't really want to start up things again, just log the problemLikelihood
O
2

One thing I would ask is, ignoring the question of termination and so on, what do you think an appropriate response is if your program can't close its DB connection, either due to normal destruction or exceptional destruction.

You seem to rule out "merely logging" and are disinclined to terminate, so what do you think is the best thing to do?

I think if we had an answer to that question then we would have a better idea of how to proceed.

No strategy seems particularly obvious to me; apart from anything else, I don't really know what it means for closing a database connection to throw. What is the state of the connection if close() throws? Is it closed, still open, or indeterminate? And if it's indeterminate, is there any way for the program to revert to a known state?

A destructor failing means that there was no way to undo the creation of an object; the only way to return the program to a known (safe) state is to tear down the entire process and start over.

Ophicleide answered 5/10, 2008 at 13:47 Comment(1)
The proper thing to do would be to report the situation to the caller, if there were a way to do that. The code where the exception is thrown generally has no reason to believe that instant application exist would be the least harmful action, but there's no reason to expect that a caller who isn't notified of the cleanup exception will do the right thing. That C++ doesn't provide the mechanisms necessary to notify the caller of what happened doesn't mean that such mechanisms aren't needed to do the semantically-correct thing--it merely makes doing the semantically-correct thing impossible.Flowerdeluce
W
1

What are the reasons why your destruction might fail? Why not look to handling those before actually destructing?

For example, closing a database connection may be because:

  • Transaction in progress. (Check std::uncaught_exception() - if true, rollback, else commit - these are the most likely desired actions unless you have a policy that says otherwise, before actually closing the connection.)
  • Connection is dropped. (Detect and ignore. The server will rollback automatically.)
  • Other DB error. (Log it so we can investigate and possibly handle appropriately in the future. Which may be to detect and ignore. In the meantime, try rollback and disconnect again and ignore all errors.)

If I understand RAII properly (which I might not), the whole point is its scope. So it's not like you WANT transactions lasting longer than the object anyway. It seems reasonable to me, then, that you want to ensure closure as best you can. RAII doesn't make this unique - even without objects at all (say in C), you still would try to catch all error conditions and deal with them as best as you can (which is sometimes to ignore them). All RAII does is force you to put all that code in a single place, no matter how many functions are using that resource type.

Warnke answered 5/10, 2008 at 14:6 Comment(0)
P
0

If one really needs to deal with some errors during the finalization process, it should not be done within the destructor. Instead, a separate function that returns an error code or may throw should be used instead. To reuse the code, you can call this function inside the destructor, but you must not allow the exception to leak out.

As some people mentioned, it is not really resource deallocation, but something like resource commit during exit. As other people mentioned, what can you do if saving fails during a forced power-off? There are probably no all-satisfying answers, but I would suggest one of the following approaches:

  • Just allow the failure and loss to happen
  • Save the unsaved part to somewhere else and allow the recovery to happen later (see the other approach if this does not work either)

If you do not like either of this approaches, make your user explicitly save. Tell them not to rely on the auto-save during a power-off.

Paillette answered 1/10, 2008 at 19:14 Comment(0)
T
0

You can tell whether there is currently an exception in flight (e.g. we are between the throw and catch block performing stack unwinding, perhaps copying exception objects, or similar) by checking

bool std::uncaught_exception()

If it returns true, throwing at this point will terminate the program, If not, it's safe to throw (or at least as safe as it ever is). This is discussed in Section 15.2 and 15.5.3 of ISO 14882 (C++ standard).

This doesn't answer the question of what to do when you hit an error while cleaning up an exception, but there really aren't any good answers to that. But it does let you distinguish between normal exit and exceptional exit if you wait to do something different (like log&ignore it) in the latter case, rather than simply panicing.

Timotheus answered 1/10, 2008 at 19:33 Comment(1)
NOTE: see Martin's post above... this can return true even in a try{..} if an exception unwind is active.Lampert

© 2022 - 2024 — McMap. All rights reserved.