www.digitalmars.com

D Programming Language 1.0


Last update Sun Dec 30 20:34:43 2012

Exception Safe Programming

Exception safe programming is programming so that if any piece of code that might throw an exception does throw an exception, then the state of the program is not corrupted and resources are not leaked. Getting this right using traditional methods often results in complex, unappealing and brittle code. As a result, exception safety often is either buggy or simply ignored for the sake of expediency.

Example

For example, if there's a Mutex m that must be acquired and held for a few statements, then released:
void abc()
{
    Mutex m = new Mutex;
    lock(m);	// lock the mutex
    foo();	// do processing
    unlock(m);	// unlock the mutex
}

If foo() throws an exception, then abc() exits via exception unwinding, unlock(m) is never called and the Mutex is not released. This is a fatal problem with this code.

The RAII (Resource Acquisition Is Initialization) idiom and the try-finally statement form the backbone of the traditional approaches to writing exception safe programming.

RAII is scoped destruction, and the example can be fixed by providing a Lock class with a destructor that gets called upon the exit of the scope:

class Lock
{
    Mutex m;

    this(Mutex m)
    {
	this.m = m;
	lock(m);
    }

    ~this()
    {
	unlock(m);
    }
}

void abc()
{
    Mutex m = new Mutex;
    scope L = new Lock(m);
    foo();	// do processing
}
If abc() is exited normally or via an exception thrown from foo(), L gets its destructor called and the mutex is unlocked. The try-finally solution to the same problem looks like:
void abc()
{
    Mutex m = new Mutex;
    lock(m);	// lock the mutex
    try
    {
	foo();	// do processing
    }
    finally
    {
	unlock(m);	// unlock the mutex
    }
}

Both solutions work, but both have drawbacks. The RAII solution often requires the creation of an extra dummy class, which is both a lot of lines of code to write and a lot of clutter obscuring the control flow logic. This is worthwhile to manage resources that must be cleaned up and that appear more than once in a program, but it is clutter when it only needs to be done once. The try-finally solution separates the unwinding code from the setup, and it can often be a visually large separation. Closely related code should be grouped together.

The scope exit statement is an easier approach:

void abc()
{
    Mutex m = new Mutex;

    lock(m);	// lock the mutex
    scope(exit) unlock(m);	// unlock on leaving the scope

    foo();	// do processing
}
The scope(exit) statement is executed at the closing curly brace upon normal execution, or when the scope is left due to an exception having been thrown. It places the unwinding code where it aesthetically belongs, next to the creation of the state that needs unwinding. It's far less code to write than either the RAII or try-finally solutions, and doesn't require the creation of dummy classes.

Example

The next example is in a class of problems known as transaction processing:
Transaction abc()
{
    Foo f;
    Bar b;

    f = dofoo();
    b = dobar();

    return Transaction(f, b);
}

Both dofoo() and dobar() must succeed, or the transaction has failed. If the transaction failed, the data must be restored to the state where neither dofoo() nor dobar() have happened. To support that, dofoo() has an unwind operation, dofoo_undo(Foo f) which will roll back the creation of a Foo.

With the RAII approach:

class FooX
{
    Foo f;
    bool commit;

    this()
    {
	f = dofoo();
    }

    ~this()
    {
	if (!commit)
	    dofoo_undo(f);
    }
}

Transaction abc()
{
    scope f = new FooX();
    Bar b = dobar();
    f.commit = true;
    return Transaction(f.f, b);
}
With the try-finally approach:
Transaction abc()
{
    Foo f;
    Bar b;

    f = dofoo();
    try
    {
	b = dobar();
	return Transaction(f, b);
    }
    catch (Object o)
    {
	dofoo_undo(f);
	throw o;
    }
}

These work too, but have the same problems. The RAII approach involves the creation of dummy classes, and the obtuseness of moving some of the logic out of the abc() function. The try-finally approach is wordy even with this simple example; try writing it if there are more than two components of the transaction that must succeed. It scales poorly.

The scope(failure) statement solution looks like:

Transaction abc()
{
    Foo f;
    Bar b;

    f = dofoo();
    scope(failure) dofoo_undo(f);

    b = dobar();

    return Transaction(f, b);
}
The dofoo_undo(f) only is executed if the scope is exited via an exception. The unwinding code is minimal and kept aesthetically where it belongs. It scales up in a natural way to more complex transactions:
Transaction abc()
{
    Foo f;
    Bar b;
    Def d;

    f = dofoo();
    scope(failure) dofoo_undo(f);

    b = dobar();
    scope(failure) dobar_unwind(b);

    d = dodef();

    return Transaction(f, b, d);
}

Example

The next example involves temporarily changing the state of some object. Suppose there's a class data member verbose, which controls the emission of messages logging the activity of the class. Inside one of the methods, verbose needs to be turned off because there's a loop that would otherwise cause a blizzard of messages to be output:
class Foo
{
    bool verbose;	// true means print messages, false means silence
    ...
    bar()
    {
	auto verbose_save = verbose;
	verbose = false;
	... lots of code ...
	verbose = verbose_save;
    }
}
There's a problem if Foo.bar() exits via an exception - the verbose flag state is not restored. That's easily fixed with scope(exit):
class Foo
{
    bool verbose;	// true means print messages, false means silence
    ...
    bar()
    {
	auto verbose_save = verbose;
	verbose = false;
	scope(exit) verbose = verbose_save;

	...lots of code...
    }
}

It also neatly solves the problem if ...lots of code... goes on at some length, and in the future a maintenance programmer inserts a return statement in it, not realizing that verbose must be reset upon exit. The reset code is where it belongs conceptually, rather than where it gets executed (an analogous case is the continuation expression in a ForStatement). It works whether the scope is exited by a return, break, goto, continue, or exception.

The RAII solution would be to try and capture the false state of verbose as a resource, an abstraction that doesn't make much sense. The try-finally solution requires arbitrarily large separation between the conceptually linked set and reset code, besides requiring the addition of an irrelevant scope.

Example

Here's another example of a multi-step transaction, this time for an email program. Sending an email consists of two operations:
  1. Perform the SMTP send operation.
  2. Copy the email to the "Sent" folder, which in POP is on the local disk, and in IMAP is also remote.

Messages should not appear in "Sent" that haven't been actually sent, and sent messages must actually appear in "Sent".

Operation (1) is not undoable because it's a well-known distributed computing issue. Operation (2) is undoable with some degree of reliability. So we break the job down into three steps:

  1. Copy the message to "Sent" with a changed title "[Sending] <Subject>". This operation ensures there's space in the client's IMAP account (or on the local disk), the rights are proper, the connection exists and works, etc.
  2. Send the message via SMTP.
  3. If sending fails, delete the message from "Sent". If the message succeeds, change its title from "[Sending] <Subject>" to "<Subject>". Both of these operation have a high probability to succeed. If the folder is local, the probability of success is very high. If the folder is remote, probability is still vastly higher than that of step (1) because it doesn't involve an arbitrarily large data transfer.
class Mailer
{
    void Send(Message msg)
    {
	{
	    char[] origTitle = msg.Title();
	    scope(exit) msg.SetTitle(origTitle);
	    msg.SetTitle("[Sending] " ~ origTitle);
	    Copy(msg, "Sent");
	}
	scope(success) SetTitle(msg.ID(), "Sent", msg.Title);
	scope(failure) Remove(msg.ID(), "Sent");
	SmtpSend(msg);	// do the least reliable part last
    }
}
This is a compelling solution to a complex problem. Rewriting it with RAII would require two extra silly classes, MessageTitleSaver and MessageRemover. Rewriting the example with try-finally would require nested try-finally statements or use of an extra variable to track state evolution.

Example

Consider giving feedback to the user about a lengthy operation (mouse changes to an hourglass, window title is red/italicized, ...). With scope(exit) that can be easily done without needing to make an artificial resource out of whatever UI state element used for the cues:
void LongFunction()
{
    State save = UIElement.GetState();
    scope(exit) UIElement.SetState(save);
    ...lots of code...
}
Even more so, scope(success) and scope(failure) can be used to give an indication if the operation succeeded or if an error occurred:
void LongFunction()
{
    State save = UIElement.GetState();
    scope(success) UIElement.SetState(save);
    scope(failure) UIElement.SetState(Failed(save));
    ...lots of code...
}

When to use RAII, try-catch-finally, and Scope

RAII is for managing resources, which is different from managing state or transactions. try-catch is still needed, as scope doesn't catch exceptions. It's try-finally that becomes redundant.

Acknowledgements

Andrei Alexandrescu argued about the usefulness of these constructs on the Usenet and also defined their semantics in terms of try/catch/finally in a series of posts to comp.lang.c++.moderated under the title A safer/better C++? starting Dec 6, 2005. D implements the idea with a slightly modified syntax following its creator's experiments with the feature and useful suggestions from the D programmer community, especially Dawid Ciezarkiewicz and Chris Miller.

I am indebted to Scott Meyers for teaching me about exception safe programming.

References:

  1. Generic<Programming>: Change the Way You Write Exception-Safe Code Forever by Andrei Alexandrescu and Petru Marginean
  2. "Item 29: Strive for exception-safe code" in Effective C++ Third Edition, pg. 127 by Scott Meyers




Forums | Comments |  D  | Search | Downloads | Home