Octopull/C++: "Here be Dragons"
Home C++ articles Java articles Agile articles Bridge lessons

"Here be Dragons"by Alan Griffiths

"The use of animals in maps was commonplace from the earliest times. Man's deepest fears and superstitions concerning wide expanses of uncharted seas and vast tracts of 'terra incognita' were reflected in the monstrous animals that have appeared on maps ever since the Mappa Mundi." (Roderick Barron in "Decorative Maps")

For many developers C++ exception handling is like this - a Dark Continent with poor maps and rumours of ferocious beasts. I'm Alan Griffiths and I'm your guide to the landmarks and fauna of this region.

In order to discuss exception safety we need to cover a lot of territory. The next section identifies the "exception safe" mountains in the distance. Please don't skip it in the hope of getting to the good stuff - if you don't take the time to get your bearings now you'll end up in the wastelands.

Once I've established the bearings I'll show you a well-trodden path that leads straight towards the highest peak and straight through a tar pit. From experience, I've concluded that everyone has to go down this way once. So I'll go with you to make sure you come back. Not everyone comes back; some give up on the journey, others press on deeper and deeper into the tar pit until they sink from sight.

On our journey I'll tell you the history of how the experts sought for a long time before they discovered a route that bypasses that tar pit and other obstacles. Most maps don't show it yet, but I'll show you the signs to look out for. I'll also show you that the beasts are friendly and how to enlist their aid.

If you look into the distance you'll see a range of peaks, these are the heights of exception safety and are our final destination. But before we proceed on our trek let me point out two of the most important of these peaks, we'll be using them as landmarks on our travels…

The mountains (landmarks of exception safety)

The difficulty in writing exception safe code isn't in writing the code that throws an exception, or in writing the code that catches the exception to handle it. There are many sources that cover these basics. I'm going to address the greater challenge of writing the code that lies in between the two.

Imagine for a moment the call stack of a running program, function a() has called function b(), b() has called c(), and so on, until we reach x(); x() encounters a problem and throws an exception. This exception causes the stack to unwind, deleting automatic variables along the way, until the exception is caught and dealt with by a().

I'm not going to spend any time on how to write functions a() or x(). I'm sure that the author of x() has a perfectly good reason for throwing an exception (running out of memory, disc storage, or whatever) and that the author of a() knows just what to do about it (display: "Sorry, please upgrade your computer and try again!").

The difficult problem is to write all the intervening functions in a way that ensures that something sensible happens as a result of this process. If we can achieve this we have "exception safe" code. Of course, that begs the question "what is something sensible?" To answer this let us consider a typical function f() in the middle of the call stack. How should f() behave?

Well, if f() were to handle the exception it might be reasonable for it to complete its task by another method (a different algorithm, or returning a "failed" status code). However, we are assuming the exception won't be handled until we reach a(). Since f() doesn't run to completion we might reasonably expect that:

1. f() doesn't complete its task.

2. If f() has opened a file, acquired a lock on a mutex, or, more generally; if f() has "allocated a resource" then the resource should not leak. (The file must be closed, the mutex must be unlocked, etc.)

3. If f() changes a data structure, then that structure should remain useable - e.g. no dangling pointers.

In summary: If f() updates the system state, then the state must remain valid. Note that isn't quite the same as correct - for example, part of an address may have changed leaving a valid address object containing an incorrect address.

I'm going to call these conditions the basic exception safety guarantee, this is the first, and smaller of our landmark mountains. Take a good look at it so that you'll recognise it later.

The basic exception safety guarantee may seem daunting but not only will we reach this in our travels, we will be reaching an even higher peak called the strong exception safety guarantee that places a more demanding constraint on f():

4. If f() terminates by propagating an exception then it has made no change to the state of the program.

Note that it is impossible to implement f() to deliver either the basic or strong exception safety guarantees if the behaviour in the presence of exceptions of the functions it calls isn't known. This is particularly relevant when the client of f() (that is e()) supplies the functions to be called either as callbacks, as implementations of virtual member functions, or via template parameters. In such cases the only recourse is to document the constraints on them - as, for example, the standard library does for types supplied as template parameters to the containers.

If we assume a design with fully encapsulated data then each function need only be held directly responsible for aspects of the object of which it is a member. For the rest, the code in each function must rely on the functions it calls to behave as documented. (We have to rely on documentation in this case, since in C++ there is no way to express these constraints in the code.)

We'll rest here a while, and I'll tell you a little of the history of this landscape. Please take the time to make sure that you are familiar with these two exception safety guarantees. Later, when we have gained some altitude we will find that there is another peak in the mountain range: the no-throw exception safety guarantee - as the name suggests this implies that f() will never propagate an exception.

A history of this territory

The C++ people first came to visit the land of exceptions around 1990 when Margaret Ellis and Bjarne Stroustrup published the "Annotated Reference Manual" [1]. Under the heading "experimental features" this described the basic mechanisms of exceptions in the language. In this early bestiary there is an early description of one of the friendly beasts we shall be meeting later on: it goes by the strange name of RESOURCE ACQUISITION IS INITIALISATION.

By the time the ISO C++ Standards committee circulated "Committee Draft 1" in early 1995 C++ people were truly living in exception land. They hadn't really mapped the territory or produced an accurate bestiary but they were committed to staying and it was expected that these would soon be available.

However, by late 1996 when "Committee Draft 2" was circulated the difficulties of this undertaking had become apparent. Around this time there came a number of reports from individual explorers. For example: Dave Abrahams identified the mountains we are using as landmarks in his paper "Exception Safety in STLPort" [2] although the basic exception safety guarantee was originally dubbed the "weak exception safety guarantee".

Some other studies of the region were produced by H Muller [3], Herb Sutter [4] and [5]. A little later came a sighting of another of the friendly beast that we will meet soon called ACQUISITION BEFORE RELEASE. This beast was first known by a subspecies named it COPY BEFORE RELEASE and was identified by Kevlin Henney [6] it is distinguished by the resources allocated being copies of dynamic objects.

By the time the ISO C++ Language Standard was published in 1998 the main tracks through the territory had been charted. In particular there are clauses in the standard guaranteeing the behaviour of the standard library functions in the presence of exceptions. Also, in a number of key places within the standard, special mention is made of another friendly beast - SWAP in its incarnation as the std::swap() template function. We will be examining SWAP after our detour through the tar pit.

Since the publication of the ISO standard more modern charts have been produced: the author in an early version of this article [7]. A similar route is followed by Bjarne Stroustrup [8]. Herb Sutter [9] takes a different route, but the same landmarks are clearly seen.

OK, that's enough rest, we are going to take the obvious path and head directly towards the strong exception safety guarantee.

The tar pit

It is time to consider an example function, and for this part of the journey I have chosen the assignment operator for the following class:

class PartOne   { /* omitted */ };
class PartTwo   { /* omitted */ };

class Whole
{
public:
    // ...Lots omitted...

    Whole& operator=(const Whole& rhs);
    
private:
    PartOne*    p1;
    PartTwo*    p2;
};

Those of you that have lived in the old country will know the classical form for the assignment operator. It looks something like the following:

Whole& Whole::operator=(const Whole& rhs)
{
    if (&rhs != this)
    {
        delete p1;
        delete p2;
        p1 = new PartOne(*rhs.p1);
        p2 = new PartTwo(*rhs.p2);
    }
    return *this;
}

If you've not seen this before, don't worry because in the new land it is not safe. Either of the new expressions could reasonably throw (since at the very least they attempt to allocate memory) and this would leave the p1 and p2 pointers dangling. In theory the "delete" expressions could also throw - but in this article we will assume that destructors never propagate exceptions. (See: "destructors that throw exceptions".)

The obvious solution to the problems caused by an exception being propagated is to catch the exception and do some clean up before throwing it again. After doing the obvious we have:

Whole& Whole::operator=(const Whole& rhs)
{
    if (&rhs != this)
    {
        PartOne* t1 = new PartOne(*rhs.p1);

        try 
        {
            PartTwo* t2 = new PartTwo(*rhs.p2);

            delete p1;
            delete p2;

            p1 = t1;
            p2 = t2;
        }
        catch (...)
        {
            delete t1;
            throw;
        }
    }
    return *this;
}

Let's examine why this works:

1. An exception in the first new expression isn't a problem - we haven't yet allocated any resources or modified anything.

2. If an exception is propagated from the second new expression, we need to release t1. So we catch it, delete t1 and throw the exception again to let it propagate.

3. We are assuming that destructors don't throw, so we pass over the two deletes without incident. Similarly the two assignments are of base types (pointers) and cannot throw an exception.

4. The state of the Whole isn't altered until we've done all the things that might throw an exception.

If you peer carefully through the undergrowth you can see the first of the friendly beasts. This one is called ACQUISITION BEFORE RELEASE. It is recognised because the code is organised so that new resources (the new PartOne and PartTwo) are successfully acquired before the old ones are released.

We've achieved the strong exception safety guarantee on our first attempt! But there is some black sticky stuff on our boots.

Tar!

There are problems lying just beneath the surface of this solution. I chose an example that would enable us to pass over the tar pit without sinking too deep. Despite this, we've incurred costs: the line count has doubled and it takes a lot more effort to understand the code well enough to decide that it works.

If you want to, you may take some time out to convince yourself of the existence of the tar pit - I'll wait. Try the analogous example with three pointers to parts or replacing the pointers with two parts whose assignment operators may throw exceptions. With real life examples things get very messy very quickly.

Many people have reached this point and got discouraged. I agree with them: routinely writing code this way is not reasonable. Too much effort is expended on exception safety housekeeping chores like releasing resources. If you hear that "writing exception safe code is hard" or that "all those try...catch blocks take up too much space" you are listening to someone that has discovered the tar pit.

I'm now going to show you how exception handling allows you to use less code (not more), and I'm not going to use a single try…catch block for the rest of the article! (In a real program the exception must be caught somewhere - like function a() in the discussion above, but most functions simply need to let the exceptions pass through safely.)

The royal road

There are three "golden rules":

1. Destructors may not propagate exceptions,

2. The states of two instances of a class may be swapped without an exception being thrown,

3. An object may own at most one resource,

We've already met the first rule.

The second rule isn't obvious, but is the basis on which SWAP operates and is key to exception safety. The idea of SWAP is that for two instances of a class that owns resources exchanging the states is feasible without the need to allocate additional resources. Since nothing needs to be allocated, failure needn't be an option and consequently neither must throwing an exception. (It is worth mentioning that the no-throw guarantee is not feasible for assignment, which may have to allocate resources.)

If you look at the ISO C++ Language Standard, you'll find that std::swap() provides the no-throw guarantee for fundamental types and for relevant types in the standard library. This is achieved by overloading std::swap() - e.g. there is a template corresponding to each of the STL containers. This looks like a good way to approach SWAP but introducing additional overloads of std::swap() is not permitted by the language standard. The standard does permit to explicit specialisation of an existing std::swap() template function on user defined classes and this is what I would recommend doing where applicable (there is an example below). The standards committee is currently considering a defect report that addresses the problem caused by these rules for the authors of user defined template classes. (See: Standard Algorithms and User Defined Template Classes.)

The third rule addresses the cause of all the messy exception handling code we saw in the last section. It was because creating a new second part might fail that we wrote code to handle it and doubled the number of lines in the assignment operator.

We'll now revisit the last example and make use of the above rules. In order to conform to the rule regarding ownership of multiple objects we'll delegate the responsibility of resource ownership to a couple of helper classes. I'm using the std::auto_ptr<> template to generate the helper classes here because it is standard, not because it is the ideal choice. (See: "The Trouble With std::auto_ptr<>" for reasons to avoid using auto_ptr<> in this context.)

class Whole {
public:
    // ...Lots omitted...

    Whole& operator=(const Whole& rhs);

private:
    std::auto_ptr<PartOne>    p1;
    std::auto_ptr<PartTwo>    p2;
};


Whole& Whole::operator=(const Whole& rhs)
{
    std::auto_ptr<PartOne> t1(new PartOne(*rhs.p1));
    std::auto_ptr<PartTwo> t2(new PartTwo(*rhs.p2));

    std::swap(p1, t1);
    std::swap(p2, t2);

    return *this;
}

Not only is this shorter than the original exception-unsafe example, it meets the strong exception safety guarantee.

Look at why it works:

1. There are no leaks: whether the function exits normally, or via an exception, t1 and t2 will delete the parts they currently own.

2. The swap expressions cannot throw (second rule).

3. The state of the Whole isn't altered until we've done all the things that might throw an exception.

Oh, by the way, I've not forgotten about self-assignment. Think about it - you will see the code works without a test for self-assignment. Such a test may be a bad idea: assuming that self-assignment is very rare in real code and that the branch could have a significant cost Francis Glassborow suggested a similar style of assignment operator as a speed optimisation [10]. Following on from this, Kevlin Henney explored its exception safety aspects in [11], [12] and [6].

We are on much firmer ground than before: it isn't hard to see why the code works and generalising it is simple. You should be able to see how to manage a Whole with three auto_ptrs to Parts without breaking stride.

You can also see another of the friendly beasts for the first time. Putting the allocation of a resource (here a new expression) into the initialiser of an instance of a class (eg auto_ptr<PartOne>) that will delete it on destruction is RESOURCE ACQUISITION IS INITIALISATION. And, of course, we can once again see ACQUISITION BEFORE RELEASE.

(Yes, in this case we could use assignment instead of SWAP to make the updates. However with a more complex type SWAP is necessary, as we shall see later. I use SWAP in this example for consistency.)

The assignment operator - a special case

Before I go on to deal with having members that may throw when updated, I've a confession I need to make. It is possible, and usual, to write the assignment operator more simply than the way I've just demonstrated. The above method is more general than what follows and can be applied when only some aspects of the state are being modified. The following applies only to assignment:

Whole& Whole::operator=(const Whole& rhs)
{
    Whole(rhs).swap(*this);
    return *this;
}

Remember the second rule: Whole is a good citizen and provides for SWAP (by supplying the swap() member function). I also make use of the copy constructor - but it would be a perverse class design that supported assignment but not copy construction. I'm not sure whether the zoologists have determined the relationship between SWAP and copying here, but the traveller won't go far wrong in considering COPY AND SWAP as species in it own right.

For completeness, I'll show the methods used above:

void Whole::swap(Whole& that)
{
    std::swap(p1, that.p1);
    std::swap(p2, that.p2);
}

Whole::Whole(const Whole& rhs)
:   p1(new PartOne(*rhs.p1)),
    p2(new PartTwo(*rhs.p2))
{
}

One further point about making Whole a good citizen is that we need to specialise std::swap() to work through the swap() member function. By default std::swap() will use assignment - and not deliver the no-throw guarantee we need for SWAP. The standard allows us to specialise existing names in the std namespace on our own types, and it is good practice to do so in the header that defines the type.

namespace std
{
    template<>
    inline void swap(exe::Whole& lhs, exe::Whole& rhs)
    {
        lhs.swap(rhs);
    }
}

This avoids any unpleasant surprises for client code that attempts to swap() two Wholes.

Although we've focused on attaining the higher peak of strong exception safety guarantee, we've actually covered all the essential techniques for achieving either strong or basic exception safety. The remainder of the article shows the same techniques being employed in a more complex example and gives some indication of the reasons you might choose to approach the lesser altitudes of basic exception safety.

In bad weather

We can't always rely on bright sunshine, or on member variables that are as easy to manipulate as pointers. Sometimes we have to deal with rain and snow, or base classes and member variables with internal state.

To introduce a more complicated example, I'm going to elaborate the Whole class we've just developed by adding methods that update p1 and p2. Then I'll derive an ExtendedWhole class from it that also contains an instance of another class: PartThree. We'll be assuming that operations on PartThree are exception safe, but, for the purposes of discussion, I'll leave it open whether PartThree offers the basic or the strong exception safety guarantee.

Whole& Whole::setP1(const PartOne& value)
{
    p1.reset(new PartOne(value));
    return *this;
}


Whole& Whole::setP2(const PartTwo& value)
{
    p2.reset(new PartTwo(value));
    return *this;
}


class ExtendedWhole : private Whole
{
public:

    // Omitted constructors & assignment

    void swap(const ExtendedWhole& rhs);

    void setParts(
        const PartOne& p1,
        const PartTwo& p2,
        const PartThree& p3);

private:
    int       count;
    PartThree body;
};

The examples we've looked at so far are a sufficient guide to writing the constructors and assignment operators. We are going to focus on two methods: the swap() member function and a setParts() method that updates the parts.

Writing swap() looks pretty easy - we just swap the base class, and each of the members. Since each of these operations is "no-throw" the combination of them is also "no-throw".

void ExtendedWhole::swap(ExtendedWhole& rhs)
{
    Whole::swap(rhs);
    std::swap(count, rhs.count);
    std::swap(body,  rhs.body);
}

Writing setParts() looks equally easy: Whole provides methods for setting p1 and p2, and we have access to body to set that. Each of these operations is exception safe, indeed the only one that need not make the strong exception safety guarantee is the assignment to body. Think about it for a moment: is this version of setParts() exception safe? And does it matter if the assignment to body offers the basic or strong guarantee?

void ExtendedWhole::setParts(
    const PartOne& p1,
    const PartTwo& p2,
    const PartThree& p3)
{
    setP1(p1);
    setP2(p2);
    body = p3;
}

Let's go through it together, none of the operations leak resources, and setParts() doesn't allocate any so we don't have any leaks. If an exception propagates from any of the operations, then they leave the corresponding sub-object in a useable state, and presumably that leaves ExtendedWhole useable (it is possible, but in this context implausible, to construct examples where this isn't true). However, if an exception propagates from setP2() or from the assignment then the system state has been changed. And this is so regardless of which guarantee PartThree makes.

The simple way to support the strong exception safety guarantee it to ensure that nothing is updated until we've executed all the steps that might throw an exception. This means taking copies of sub-objects and making the changes on the copies, prior to swapping the state between the copies and the original sub-objects:

void ExtendedWhole::setParts(
    const PartOne& p1,
    const PartTwo& p2,
    const PartThree& p3)
{
    Whole temp(*this);
    temp.setP1(p1).setP2(p2);
    body = p3;
    Whole::swap(temp);
}

Once again does it matter if the assignment to body offers the basic or strong guarantee? Yes it does, if it offers the strong guarantee then all is well with the above, if not then the assignment needs to be replaced with COPY AND SWAP vis:

PartThree(p3).swap(body);

Once again we have attained the highest peak, but this may not be healthy. On terrestrial mountains above a certain height there is a "death zone" where the supply of oxygen is insufficient to support life. Something similar happens with exception safety: there is a cost to implementing the strong exception safety guarantee. Although the code you write isn't much more complicated than the 'basic' version, additional objects are created and these allocate resources at runtime. This causes the program to make more use of resources and to spend time allocating and releasing them.

Trying to remain forever at high altitude will drain the vitality. Fortunately, the basic exception safety guarantee is below the death zone: when one makes a composite operation whose parts offer this guarantee one automatically attains the basic guarantee (as the first version of setParts() shows this is not true of the strong guarantee). From the basic guarantee there is an easy climb from this level to the strong guarantee by means of COPY AND SWAP.

Looking back

Before we descend from the peak of strong exception safety guarantee and return to our starting point look back over the route we covered. In the distance you can see the well-trampled path that led to the tar pit and just below us the few tracks leading from the tar pit up a treacherous scree slope to where we stand. Off to the left is the easier ridge path ascending from basic exception safety guarantee and beyond that the road that led us past the tar pit. Fix these landmarks in your mind and remember that the beasts we met are not as fierce as their reputations.


References

[1] The Annotated C++ Reference Manual Ellis & Stroustrup ISBN 0-201-51459-1
[2] Exception Safety in STLPort Dave Abrahams http://www.stlport.org/doc/exception_safety.html
[3] Ten rules for handling exception handling successfully H Muller C++ Report Jan.'96
[4] Designing exception-safe Generic Containers Herb Sutter C++ Report Sept.'97
[5] More exception-safe Generic Containers Herb Sutter C++ Report Nov-Dec.'97
[6] Creating Stable Assignments Kevlin Henney C++ Report June'98
[7] The safe path to C++ exceptions Alan Griffiths EXE Dec.'99
[8] The C++ Programming Language (3rd Edition)
appendix E "Standard Library Exception Safety"
Bjarne Stroustrup
(this appendix dies not appear in early printings, but is available on the web at http://www.research.att.com/~bs/3rd_safe.pdf)
[9] Exceptional C++" Herb Sutter ISBN 0-201-61562-2
[10] The Problem of Self-Assignment" Francis Glassborow Overload 19 ISSN 1354-3172
[11] Self Assignment? No Problem!" Kevlin Henney Overload 20 ISSN 1354-3172
[12] Self Assignment? No Problem!" Kevlin Henney Overload 21 ISSN 1354-3172

Top   The mountains   A history of this territory   Tar!   The royal road   The assignment operator   In bad weather   Looking back

Box out 1 - Destructors that throw exceptions

Exceptions propagating from destructors cause a number of problems. For example, consider a Whole that holds pointers to a PartOne, a PartTwo, and a PartThree that it owns (ie it must delete them). If the destructors of the parts propagate exceptions, we would have trouble just writing a destructor for Whole. If more than one destructor throws, we must suppress at least one exception while remembering to destroy the third part. Writing update methods (like assignment) under such circumstances is prohibitively difficult or impossible.

There many situations where an exception propagating from a destructor is extremely inconvenient - my advice is not to allow classes that behave in this manner into your system. (If forced to, you can always 'wrap' them in a well behaved class of your own.)

If you look at the standard library containers, you'll find that they place certain requirements on the types that are supplied as template parameters. One of these is that the destructor doesn't throw exceptions. There is a good reason for this: it is hard to write code that manipulates objects that throw exceptions when you try to destroy them. In many cases, it is impossible to write efficient code under such circumstances.

In addition, the C++ exception handling mechanism itself objects to destructors propagating exception during the "stack unwinding" process. Indeed, unless the application developer takes extraordinary precautions the application will be terminated in a graceless manner.

There is no advantage in allowing destructors to propagate exceptions and a whole host of disadvantages. It should be easy to achieve: in most cases all a destructor should be doing is destroying other objects whose destructors shouldn't throw, or releasing resources - and if that fails an exception won't help.

Apart from the practicalities what does an exception from a destructor mean? If I try to destroy an object and this fails what am I supposed to do? Try again?

Destructors that throw exceptions? Just say no.

Top   The mountains   A history of this territory   Tar!   The royal road   The assignment operator   In bad weather   Looking back

Box out 2 - Standard Algorithms and User Defined Template Classes

The std::swap() template functions are one example of an algorithm implemented by the standard library. It is also an example of one where there is a good reason for C++ users to endeavour to provide an implementation specific to the needs of the classes and class templates that they develop. This need is particularly significant to developers of extension libraries - who would like to ensure that what they develop will both work well with the library and with other extension libraries.

So consider the plight of a developer who is writing a template class Foo<> that takes a template parameter T and wishes to SWAP two instances of T. Now Foo is being instantiated with a fundamental type, or with an instance of any swappable type from the standard library the correct function can be resolved by writing:

using std::swap;
swap(t1, t2);

However, the primary template std::swap() that will be instantiated for other types is guaranteed to use copy construction and assignment unless an explicit specialisation has been provided (and this is impractical if T is a specialisation of a template class). As we have seen, copy construction and assignment probably won't meet the requirements of SWAP. Now this won't always matter, because a language mechanism "Argument-dependent name lookup" (commonly know as "Koenig Lookup") might introduce into overload resolution a function called swap() from the namespace in which T is declared, if this is the best match for arguments of type T then it is the one that gets called and the templates in std are ignored.

Now there are three problems with this:

1. Depending on the context of the above code Koenig Lookup produces different results. (A library developer might reasonably be expected to know the implications of a class member named "swap" and how to deal with them - but many don't.) Most C++ users will simply get it wrong - without any obvious errors when only the std::swap() templates are considered.

2. The standard places no requirements on functions called "swap" in any namespace other than std - so there is no guarantee that bar::swap() will do the right thing.

3. In a recent resolution of an issue the standards committee has indicated that where one standard function/template function is required to be used by another then the use should be fully qualified (i.e. std::swap(t1, t2);) to prevent the application of Koenig Lookup. If you (or I) provide yournamespace::swap() the standard algorithms won't use it.

Since the standards committee is still considering what to do about this I can't give you a watertight recommendation. I am hoping that in the short term a "technical corrigenda" will permit users to introduce new overloads of such template functions in std. So far as I know this technique works on all current implementations - if you know of one where it doesn't please let me know. In the longer term I am hoping that the core language will be extended to permit the partial specialisation of template functions (and also that the library changes to use partial specialisation in place of overloading).

For those that follow such things this is library issue 226 (http://anubis.dkuug.dk/JTC1/SC22/WG21/docs/lwg-active.html).

Top   The mountains   A history of this territory   Tar!   The royal road   The assignment operator   In bad weather   Looking back

Box out 3 - The Trouble With std::auto_ptr<>

By historical accident, the standard library provides a single smart pointer template known as auto_ptr<>. auto_ptr<> has what I will politely describe as "interesting" copy semantics. Specifically, if one auto_ptr<> is assigned (or copy constructed) from another then they are both changed - the auto_ptr<> that originally owned the object loses ownership and becomes 0. This is a trap for the unwary traveller! There are situations that call for this behaviour, but on most occasions that require a smart pointer the copy semantics cause a problem.

When we replace PartXXX* with auto_ptr<PartXXX> in the Whole class we still need to write the copy constructor and assignment operator carefully to avoid an PartXXX being passed from one Whole to another (with the consequence that one Whole loses its PartXXX).

Worse than this, if we attempt to hide the implementation of PartXXX from the client code using a forward declaration we also need to write the destructor. If we don't, the one the compiler generates for us will not correctly destroy the PartXXX. This is because the client code causes the generation of the Whole destructor and consequently instantiates the auto_ptr<> destructor without having seen the class definition for PartXXX. The effect of this is that the destructor of PartXXX is never called. (A good compiler may give a cryptic warning about deleting an incomplete type, but the language standard requires that the code compiles - although the results are unlikely to be what the programmer intended.)

Although the standard library doesn't support our needs very well it is possible to write smart pointers which do. There are a couple of examples in the "arglib" library on my website. (Both "arg::body_part_ptr<>" and "arg::grin_ptr<>" are more suitable than std::auto_ptr<>.)

Top   The mountains   A history of this territory   Tar!   The royal road   The assignment operator   In bad weather   Looking back

Box out 4 - The cost of exception handling

Compiler support for exception handling does make the generated code bigger (figures vary around 10-15%), but only for the same code. However, code isn't written the same way without exceptions - for example, since constructors cannot return an error code, idioms such as "two phase construction" are required. I have here a comparable piece of code to the final example that has been handed down the generations from a time before the introduction of exception handling to C++. (Actually I've made it up - but I was around back then and remember working with code like this, so it is an authentic fake.)

int ExtendedWhole::setParts(
    const PartOne& p1,
    const PartTwo& p2,
    const PartThree& p3)
{
    Whole tw;
    int rcode = tw.init(*this);
    
    if (!rcode) rcode = tw.setP1(p1);

    if (!rcode) rcode = tw.setP2(p2);

    if (!rcode)
    {
        PartThree t3;
        rcode = t3.copy(p3);

        if (!rcode)
        {
            Whole::swap(tw);
            body.swap(t3);
        }
    }
    
    return rcode;
}

To modern eyes the need to repeat this testing & branch on return codes looks very like the tar-pit we encountered earlier - it is verbose, hard to validate code. I'm not aware of any trials where comparable code was developed using both techniques, but my expectation is that the saving in hand-written code from using exceptions significantly outweighs the extra cost in compiler-generated exception handling mechanisms.

Please don't take this as a rejection of return codes, they are one of the primary error reporting mechanisms in C++. But if an operation will only fail in exceptional circumstances (usually running out of a resource) or cannot reasonably be expected to be dealt with by the code at the call site then exceptions can greatly simplify the task.

Top   The mountains   A history of this territory   Tar!   The royal road   The assignment operator   In bad weather   Looking back


Copyright 1999, 2000 Alan Griffiths