Thursday, April 10, 2008

An Exceptional Chain of Events

It was said of yore that throwing an exception from a C++ destructor is a verie wycked and wofull thyng.

Remember goto? Some distinguished gentlemen of the C++ programming trade look down on throwing from destructors as being eviler than using a goto. Standing base for such moral a judgment is that "a throwing destructor makes it impossible to write correct code and can shut down your application without any warning" .

Destructors in C++ are one essential ingredient to the Resource Acquisition Is Initialization (RAII) idiom, which can be resumed as follows: Resources (i.e. memory, sockets, file handles, etc) are acquired during the construction of an object. If an exception is thrown from anywhere in the code during the lifetime and after the object has been fully constructed, the C++ language rules guarantee that the object's destructor will be summoned.

Thus resources can be released gracefully and graciously by said destructor, even in the distasteful and exceptional eventuality of... an exception. The Programmer is relieved from the burdensome duty of having to release resources on every single possible code path.

RAII relieves the programmer from manually preventing leaks.
(Notice how nicely relieves and leaks go together).

If a destructor, invoked as part of the automatic cleanup that entails the throwing of an exception, decides to throw its own exception, then the system has but two choices: to go into an ambiguous state (now there are two outstanding exceptions, the original one, plus the destructor-thrown one) or... throw the towel.

It is C++ Standard behavior to follow the latter course: the towel is thrown from around the waist, the user is mooned and the program calls terminate(). Hasta la Vista, Baby. And for this reason, they say your destructors should never throw.

But what if there's not way to recover?

Let's consider a generic Lock object which may look something like this:


template<typename>
class Lock : boost::noncopyable
{
T& mx_;

public:
~Lock() throw()
{
mx_.leave();
}
explicit Lock(T& mx) : mx_(mx)
{
mx_.enter();
}
};

The problem with the code above is that T::leave() may throw an exception (it may as well not throw, but one really cannot tell, since T is a template parameter).

An so I come to the conclusion of this post. I assert that the code above is just as good as it gets. Of course, T may be bound at compile time to a class that implements the leave method somewhat like this:

void Mutex::leave()
{
if (int resultCode = pthread_mutex_unlock(&mutex_))
{
throw pthread_runtime_error(resultCode);
}
}

If unlocking the resource fails, then a) something really bad must've happened (possibly a memory corruption?) and b) there is little, if anything, to do about restoring the system to a stable state.

I say let ye programme crash and burne.

What do You reckon?

5 comments:

Lucian said...

"I assert that the code above is just as good as it gets."

You could do something like this:
public:
~Lock() throw()
{
try{
mx_.leave();
} catch(...) {
//handle it
}
}

This code compiles and runs well on g++ 4.1.3.

Am I missing something?
BTW: isn't the "throw()" decorator ignored?

The Free Meme said...

Lucian, as far as I know, the throw() specification used to be ignored by older Microsoft compilers. GCC honors it.

Your code compiles of course but what happens to the overall program if a mutex is acquired but could not be released? Most likely a deadlock will occur.

The point is that if the release of resources fail in RAII, something really bad is going on and letting the program abort may not be a bad idea (unless it controls a nuclear plant).

Think of a (contrived) RAII scenario using malloc and free. Why would free() ever fail? Maybe because the internal heap structures have been trashed? How can you recover from that?

Max said...

The design of C++ exceptions shows a lot of thought into use cases different than, say, C# or Java.

I believe you can register a function other than std::terminate() for specific regions of code if you want to, or you can check if an exception is pending before you decide to add to the mix, or you can use a catch anything block, but the default behavior makes a lot of sense. Something went wrong that could not be handled at one level of abstraction, and while you you trying to fix it at another level something else went wrong.

As for the throw() specification: the standard expects things to be compiled at different times, so it is expected that a function F() may only throw three kinds of exceptions today, but be updated to throw four kinds tomorrow. Code compiled today that only expects three kinds of exceptions doesn't have to be recompiled to use the updated version, but if an exception is thrown that doesn't comply with the old exception specification then the program gets terminated (this is meant to allow ongoing maintenance). Point being, even if the exception specification is enforced, it is possible that future code updates will cause your program to crash anyway.

Max said...

The design of C++ exceptions shows a lot of thought into use cases different than, say, C# or Java.

I believe you can register a function other than std::terminate() for specific regions of code if you want to, or you can check if an exception is pending before you decide to add to the mix, or you can use a catch anything block, but the default behavior makes a lot of sense. Something went wrong that could not be handled at one level of abstraction, and while you you trying to fix it at another level something else went wrong.

As for the throw() specification: the standard expects things to be compiled at different times, so it is expected that a function F() may only throw three kinds of exceptions today, but be updated to throw four kinds tomorrow. Code compiled today that only expects three kinds of exceptions doesn't have to be recompiled to use the updated version, but if an exception is thrown that doesn't comply with the old exception specification then the program gets terminated (this is meant to allow ongoing maintenance). Point being, even if the exception specification is enforced, it is possible that future code updates will cause your program to crash anyway.

Ben Voigt said...

While exiting the program when such a condition is encountered may not be a bad idea, having your runtime environment do so for you IS bad.

The "handle it" section of lucian's proposed alternative to throwing should probably look like:

catch (...) {
save_data_to_recovery_file_and_exit();
}

If you say the process state is too corrupt to manage that, then spawn another process to sift through process memory using debug callbacks. Anything but exiting the process without saving the user's data.