More wisdom on using abstraction to achieve exception-safe designs.
Introduction
This is the third in a series of articles about handle classes and exception safety.
The first of these articles explained how handles simplify memory management in object-oriented programs. It presented a simple handle class, which could represent an object of any type that was a member of a particular inheritance hierarchy, and examined the exception-safety problems that such classes can introduce.
Our initial handle class, while exception safe, had a potentially serious drawback: copying a handle always copied the corresponding object. Our second article showed how to remove this drawback by adding a reference count to the hierarchys base class. Interestingly, adding the reference count to the inheritance hierarchy presented almost no exception-safety issues at all.
However, our reference-counting solution was intrusive, in the sense that it relied on being able to add a reference counter to the hierarchys base class. The counter in each object kept track of how many handles were attached to that object.
Intrusive handles and associated reference counts are fine strategies if we are able to change the class associated with the objects that we are managing. In this article, well show how to implement a non-intrusive reference-counting strategy. In the process, we shall see that achieving exception safety requires rather more attention.
We will continue to use the classic example of a hierarchy of geometric shapes rooted in a common base class:
// The root of our hierarchy class Shape_base { public: virtual void draw() const = 0; virtual void clone() const = 0; virtual ~Shape_base(); };
As before, calling the draw member of a shape displays the shape, and calling clone makes a new copy of the shape and returns a pointer to the copy.
Each concrete class in the shape hierarchy must define these functions appropriately:
class Circle: public Shape_base { public: // Every kind of shape must // define its own draw and // clone functions virtual void draw() const; virtual void clone() const; private: double diameter; };
Our objective is to define a handle class, named Shape, that will manage objects of classes derived from Shape_base. Our initial outline for this class will look a lot like the design in our previous article:
class Shape { public: Shape(Shape_base*); ~Shape(); void make_unique(); void draw() const { p->draw(); } // other functions as needed Shape(const Shape&); Shape& operator=(const Shape&); private: Shape_base* p; };
A handle object is attached to an object from the Shape_base hierarchy and provides access to the objects public operations. The make_unique function will generate a unique copy of the associated object from the Shape_base hierarchy; it is intended to be used by functions that need to change that object.
What remains is to figure out how to add a use count non-intrusively and to implement our make_unique function and the associated copy-control members.
Abstract Reference Counter
The problem were trying to solve is to avoid having to add a reference counter to our Shape_base class. We cant just add the counter to the handle, because then there would have to be one counter for each handle. Whenever we changed the number of handles referring to a particular object, we would have to change every counter, which would require us to know where all of the handles are. But if we knew that, we wouldnt need the counter in the first place!
If all the handles attached to an object share a single counter, and the counter is not part of the object, then the counter must be somewhere else. The most direct way to solve this problem is to treat the reference counter analogously to the Shape_base object itself, by adding a second pointer to our handle:
class Shape { public: // as above private: unsigned* refcnt; Shape_base* p; };
Then, we can arrange for make_unique and the copy-control functions to manage the counter directly. However, reference counting is a generally useful technique, so we prefer to define a new class to implement our reference counter.
Well call our new class Use; an object of this class will represent a pointer to a reference counter. From our previous article, we know what operations Use needs to support:
- Each time we create a new Use object, it should represent a newly created counter with an associated count of one.
- Copying a Use object will cause the copy to refer to the same counter as the original and will increment the shared counter appropriately.
- Assigning one Use object to another will detach the target from its old counter and attach it to the sources counter, adjusting the counts in the process and deleting the old counter if it is no longer shared.
- The destructor needs to decrement the value of the counter; if the counter goes to zero, the destructor must destroy the counter itself.
- In order to support make_unique, well need the ability to fetch, but not modify, the value of a Uses associated counter.
From this decription, we can directly write our Use class:
class Use { public: Use(): refcnt(new unsigned(1)) { } Use(const Use& u): refcnt(u.refcnt) { ++*refcnt; } ~Use() { if (*refcnt == 0) delete refcnt; } Use& operator=(const Use&); operator size_t() const { return *refcnt; } private: unsigned* refcnt; }; Use& Use::operator=(const Use& u) { ++*u.refcnt; if (*refcnt == 0) delete refcnt; refcnt = u.refcnt; return *this; }
Not surprisingly, this code looks a lot like the code from our previous article that managed the reference count. In that implementation, the refcnt was stored inside the Shape_base object and managed by members of Shape. Here weve abstracted the reference counting into its own class, but the operations are the same.
The constructor allocates and initializes a new counter. The copy constructor arranges for the new object to share the same counter as the object being copied and increments the value of the counter. The assignment operator increments the use count of its right-hand operand and then decrements the one of its left hand. Doing the operations in this order protects against self-assignment. Once the left-hand counter is decremented, we check to see if the left-hand object was the last one pointing to the counter and, if so, delete the counter. Finally, we copy the value of the pointer from the right-hand into the left-hand operand and return. The destructor decrements its counter and, if this is the last object referring to the counter, deletes it.
The only new operation is the conversion to unsigned. This operation lets us examine the value of the counter and will be used by functions, such as make_unique, that need to know whether a counter is shared. Note that the operation returns the value of the counter, not a reference to it, so that we can read but not write the value in the counter.
To see how our class works, assume that u1 and u2 refer to the same Use objects, a state of affairs that we might achieve by executing:
Use u1; Use u2 = u1;
In that case, u1 and u2 will both point to the same unsigned object with a value of 2:
If we destroy u2, then the Use destructor will decrement the reference count; because doing so does not bring the reference count to zero, it will not destroy the shared refcnt:
At this point, were we to destroy u1, the Use destructor would discover that the reference count is now zero and would delete refcnt accordingly.
Using the Reference-Counting Class
Now we can reimplement our Shape class to hold a Use object as well as a pointer to its associated Shape_base:
class Shape { public: Shape(Shape_base*): p(0) { } ~Shape() { if (u == 1) delete p; } Shape& operator=(const Shape&); void make_unique(); void draw() const { p->draw(); } // other functions as needed private: Use u; Shape_base* p; };
Before we explain the code thats here, perhaps you should pause and think about what is not here. There is no explicit copy constructor, nor is there an explicit initializer for the Use member in the default Shape constructor. Why?
The reason is that the defaults work. In the default constructor, we explicitly initialize p to point to the Shape_base object were given. Implicitly, the compiler initializes the Use member by calling the Use default constructor. That constructor allocates a new unsigned object and initializes it to 1. For example, we could create a new Shape and attach it to a newly created Circle object by executing:
Shape s = new Circle;
The resulting Shape object will point to Circle, and its Use member will point to a newly created counter initialized to 1:
The compiler-generated Shape copy constructor operates by recursively copying each element of the Shape object being copied. In the case of the Shape_base* member p, this behavior means that we copy the pointer, so that after we copy a Shape object, both Shape objects point to the same underlying Shape_base precisely the behavior that we want. Copying the Shapes Use member implicitly calls the Use copy constructor. That constructor in turn arranges for the refcnt member of its object to point to the same counter as the Use member of the Shape we are copying. At the same time it increments the value of the counter to account for the newly created handle.
In other words, if we make a copy of our previous Shape:
Shape s2(s);
then the newly created Shape will point to the same Shape_base object, and its Use member will point to the same counter. The value of the counter will have been incremented to 2:
The only other member we need to explain is the destructor:
Shape::~Shape() { if (u == 1) delete p; }
The destructor (implicitly) converts its Use member to unsigned and tests whether this is the last object to refer to its associated counter. The conversion is handled by the Use class operator unsigned, which returns the value of the counter. If that value is 1, then this is the last object using the counter, which means that this is the last object attached to our Shape_base object. In this case, the Shape destructor explicitly deletes the Shape_base. There is no need to explicitly delete the counter, because the Use destructor takes care of that. So, once the destructor completes, both the Shape_base and the counter associated with its Use member will be properly destroyed.
The Assignment Operator
Once we understand how the destructor and copy constructor work, assignment is straightforward. As always, we must ensure that we do the right thing in the case of self-assignment. If the two operands are different objects, then we must detach our handle from the left-hand object and reattach to the right-hand object, updating the reference counts appropriately:
Shape& Shape::Shape operator=(const Shape& s) { if (this != &s) { if (u == 1) // are we the last object? delete p; u = s.u; p = s.p; } return *this; }
Here we explicitly test that were not assigning to ourselves; if that test succeeds, we have to copy values from the right-hand object, first checking whether we need to do any cleanup. As in the destructor, we test the value of the counter to see whether were the last object; if so, we delete our Shape_base object. The counter itself will be cleaned up when we assign s.u to u. Recall that the Use assignment operator checks the decremented value of its left-hand operand and deletes the associated counter if that value goes to zero. All that remains is to attach the left-hand Shape_base pointer to the object in the right-hand side, which we do by assigning s.p to p.
Perceptive readers might wonder at our choice of an explicit test for self-assignment. In our first article in this series, we discussed exception-safety problems that are often present in assignment operators that use an explicit test. In that case, our assignment operator performed actions that might potentially throw:
// Unsafe implementation of assignment for our // first handle class Shape& Shape::Shape operator=(const Shape& source) { if (this != &source) { delete p; // problem here p = source->clone(); // if exception occurs here } return *this; }
We needed to call clone in order to create a new copy of the right-hand object. The fact that clone might throw meant that we had to design an assignment operator that would not change the state of the left-hand object before attempting the clone. The easiest way to solve the exception-safety problem also made self-assignment harmless:
// fix for assignment operator for our first handle class Shape& Shape::Shape operator=(const Shape& source) { Shape_base *new_p = source->clone();// might throw // No exceptions possible beyond this point delete p; p = new_p; return *this; }
Here, we start by making a copy of the right-hand operand. Only after that copy succeeds do we delete the original and then attach our pointer p to the newly created copy. This code is safe in the face of self-assignment. At worst an unneeded allocation is made, but the code both correctly handles self-assignment and is exception safe.
Now lets return to our current assignment operator. Looking at it, we see there is nothing in it that can throw an exception directly. As well see in the next section, nothing that the Use class does for us could throw either. Thus, in this particular case, the direct test for self-assignment presents no exception-safety issues and is the most direct way to handle the problem.
Exception Safety, Part 1
Before implementing the make_unique member, lets take a look at the exception safety of the rest of our implementation. Looking back at our Use class, we can see that the only operation that might throw is the allocation of a new reference-count object in the constructor:
Use::Use(): refcnt(new unsigned(1)) { }
As usual, to see whether an exception here might cause a problem, we want to see what, if any, program state has changed. In this case, there is no state aside from refcnt, and, if an exception occurs while creating the object, the compiler will clean up our Use object and propagate the exception to its caller. Thus, the Use class itself is exception safe, but users of Use must be aware that this constructor might throw and deal with any resulting exceptions appropriately.
Now lets look at our use of class Use inside our Shape handle class. That handle class does nothing directly that could throw an exception, so its only exception issues occur in the context of creating a new Use member. That, in turn, happens only inside the Shape constructor:
Shape::Shape(Shape_base*): p(0) { }
where we know that the Use default constructor is implicitly used to construct the u member of the new Shape object. An exception here is still harmless, because no program state is changed while constructing a Shape. If the Use constructor throws, then our Shape object will not be created, and the exception will propagate out to our users.
Completing Our Handle
All that remains is to implement our make_unique function. It is here that exception safety will prove much more problematic.
The purpose of make_unique is to ensure that only one Shape object is attached to the underlying Shape_base object. If necessary, make_unique must create a new, unique copy. The most direct way to code this function is:
// exception unsafe implementation void Shape::make_unique() { // are there other objects that share this one? if (u > 1) { u = Use(); // if so, create a new copy p = p->clone(); } }
Unfortunately, the direct implementation fails to be exception safe. As we know, both the Use constructor and the clone function could throw an exception. As written, a throw from the Use constructor would be harmless: we havent yet changed any state in our object, so an exception here would just cause make_unique to fail, leaving the object in a consistent state.
What about an exception from clone? In this case, wed have said that this object is the only user of its underlying Shape_base, because the assignment to u created a new counter for this object and set the counter to 1. But if clone throws, then the assignment to p is never done so p still points to the shared copy of the Shape_base object!
To see whats happening, lets assume that both s1 and s2 were handles referring to the same Shape_base object, and that we had called s2.make_unique. Given this implementation, if the clone function threw, then our objects would look like this:
Effectively, there is one more object sharing the Shape_base than our objects know about. Both s1 and s2 still share the underlying Shape_base object, but each thinks it has the only copy.
The problem here is that we have two operations that could potentially throw. We know that we can ensure exception safety by arranging to do all operations that might throw before doing anything that changes the state of an object in the system. This principle gives us a way to recode make_unique that is safe in the face of an exception either from the Use constructor or in clone:
void Shape::make_unique() { Use new_u; // might throw p = p->clone(); // if it throws, no state change yet u = new_u; // now we can change the state safely }
In this version, we perform all actions that might throw before affecting the state of the object. We first create a new Use object, which might throw, and store the object temporarily in a local variable. Then we attempt to clone the Shape_base object. If clone throws, we havent changed the current object yet, so no harm is done. If clone succeeds, then we set p to point to the newly created copy and only then do we set u to refer to the newly allocated counter.
Discussion
Our first article in this series discussed a handle class with the property that copying the handle always copies the corresponding object. Such classes are easy to write and to use, we would rather avoid needless copies if possible. To avoid such needless copies, we next looked at a simple reference-counting scheme that relied on adding a counter to the base class of the inheritance hierarchy. That implementation also added a make_unique function to our handle class. This function creates a new, unique copy of the object to which the handle is attached; the idea is to call it before executing any operation that might change that underlying object. Because make_unique copies an object, we had to take care to design this function to be safe in the face of an exception.
In this article, we created a new class Use to manage the reference count and stored a Use object directly in our handle class. This design lets us avoid intruding the reference count into our inheritance hierarchy directly. However, the new version of make_unique has two places that might potentially throw exceptions.
In this context, two is a magic number. It is fairly straightforward to make part of a program exception safe if it acquires a single resource in a way that might throw an exception: all you need to do is be sure that you dont make any state changes before you might throw the exception. We used this strategy in our previous two articles. However, acquiring a resource is usually a state change, which means that a program that intends to acquire two (or more) resources has to figure out what to do with the first resource if the attempt to acquire the second resource fails.
We solved the problem in this case by arranging for make_unique to make an extra local copy of the Use object. That way, if an exception came along in p->clone, the local variable would be automatically destroyed, and the related state changes undone. The code is thus a bit more obscure than wed like and has the disadvantage that it is now possible for exceptions to come from our handles, rather than coming exclusively from the shape classes. In our next article, well show a more unusual approach to reference counting that will solve this problem.
Andrew Koenig is a member of the Research Department at AT&Ts Shannon Laboratory, and the Project Editor of the C++ standards committee. A programmer for more than 30 years, 15 of them in C++, he has published more than 150 articles about C++ and speaks on the topic worldwide. He is the author of C Traps and Pitfalls and co-author of Ruminations on C++ and Accelerated C++.
Barbara E. Moo is an independent consultant with 20 years experience in the software field. During her nearly 15 years at AT&T, she worked on one of the first commercial projects ever written in C++, managed the companys first C++ compiler project, and directed the development of AT&Ts award-winning WorldNet Internet service business. She is co-author of Ruminations on C++ and Accelerated C++ and lectures worldwide.