Introduction
A fundamental property of C++ is that run-time polymorphism happens only through references or pointers to class objects. In consequence, object-oriented programs must deal with memory management; neither references nor pointers do so automatically. Class authors who wish to avoid managing memory can foist off the problem on their users. Indeed, doing so may be tempting, because ignoring memory management makes classes easier to write. However, such classes are harder for users to use, and the overall complexity of a system increases greatly if user code must manage the systems memory directly.
Not surprisingly, the C++ community has developed techniques to simplify memory management in object-oriented programs. One common technique of long standing is to use interface classes, typically called handles, to hide inheritance hierarchies. Such an interface class will hide a pointer to an object with a type from an inheritance hierarchy of implementation classes and will manage that objects memory in the handles constructors and destructor. This technique shields user code from the complexities of using pointers, but still obtains polymorphic behavior.
Handles are so fundamental that it should come as no surprise that they take many forms. The most direct form is for each interface object to represent a separate implementation object. Copying the interface object copies the implementation object, destroying the interface object destroys the implementation object, and so on.
If this form is not flexible enough, we can add flexibility and complexity by adding what is usually called either a use count or a reference count to the interface object. By doing so, we allow multiple interface objects to share a single implementation object, while still being able to manage memory. Such schemes reduce memory allocation and can have a significant impact on run-time performance.
We first discussed handles in the Journal of Object-Oriented Programming [1, 2], then expanded on that discussion in Ruminations on C++ [3], and, most recently, used generic handles as an implementation vehicle in Accelerated C++ [4]. During that time, our understanding of handles and how to use them has evolved. In particular, we along with others in the C++ community have begun to appreciate how much more useful a general-purpose tool such as a handle can be if it behaves sensibly in the face of exceptions.
In this series of columns, we intend first to review our understanding of how handles work, then to give examples of various kinds of handles, and finally to describe what we think is a new technique for implementing reference-counted handles that makes exception safety much easier to achieve. This first column will concentrate on the most straightforward kind of handle, in which there is a one-to-one correspondence between interface and implementation objects.
Why We Shouldnt Leave Memory Management to Our Users
In general, a handle is a kind of interface class; each object of the interface class contains a pointer to the base class of a hierarchy of implementation classes and provides operations that mirror that base class public operations. The object to which any particular interface-class object points might be a base-class object or an object of one of its derived classes.
User code operates strictly in terms of the interface class. Interface functions call the corresponding implementation functions through the stored base-class pointer, thus obtaining the correct polymorphic behavior at run time. User code operates on objects in the hierarchy only through objects of the interface type, thereby avoiding the problems inherent in dynamic allocation. The interface class does all the dynamic memory allocation so that it can control and verify that allocation.
In order to make our discussion more concrete, well use one of the oldest and most illustrative object-oriented programming examples: a program that deals with various kinds of shapes, each of which can be drawn on a display device. Of course, in a real graphical system, the classes would be much more complicated than the ones were using here, but the underlying ideas are the same.
We shall begin with an abstract base class, from which all of the others will be derived:
class Shape_base { public: virtual void draw() const = 0; virtual ~Shape_base(); };
Every class derived from Shape_base will have at least two virtual functions: draw, which will draw the shape on the display device, and the destructor, which might behave differently for different kinds of shapes.
We expect that there will be other classes derived from Shape_base:
class Circle: public Shape_base { public: virtual void draw() const; private: double diameter; }; class Polygon: public Shape_base { public: virtual void draw() const; private: std::vector<Point> vertices; };
Of course, we will want to add more members to each of these classes to make it possible to specify the shapes dimensions, control where it will be displayed, and so on. However, these skeletal classes will serve for our purposes.
Part of the reason for using an inheritance hierarchy is to make it easy to have heterogeneous containers of shapes. For example:
std::vector<Shape_base*> shapes; shapes.push_back(new Circle); shapes.push_back(new Polygon); // ... for (std::vector<Shape_base*>::const_iterator s = shapes.begin(); s != shapes.end(); ++s) { (*s)->draw(); }
Yes, we mean (*s)->draw(), not s->draw() or (*s).draw(). Heres whats going on:
The iterator s refers successively to each element of shapes. Each of these elements is a pointer that points to a shape of some kind. In this particular illustration, the initial element of shapes points to a Circle, the next element points to a Polygon, and subsequent elements presumably point to additional shapes.
It should be clear that *s is an element of shapes, and that element is itself a pointer. Therefore, the shape to which *s points is **s, and we can call that shapes draw member by calling (**s).draw or, equivalently, by calling (*s)->draw.
Suppose now that we want to form another collection that contains only some of the objects in the shapes collection. We might write something like this:
std::vector<Shape_base*> some_shapes; for (std::vector<Shape_base*>::const_iterator s = shapes.begin(); s != shapes.end(); ++s) { if (condition(s)) some_shapes.push_back(*s); }
Here, condition represents whatever test we might use to decide whether to include a shape in some_shapes. After we have executed this code, shapes still refers to all of our shapes, but some_shapes refers to some of the shapes as well:
Suppose now that were done using the shapes vector and want to deallocate it. We cannot simply run through the vector and delete every object to which it points, because some_shapes still needs some of those objects. However, we might not even be aware of the existence of the some_shapes vector, because it might have been created in a part of our program that someone else wrote.
Evidently, we can get into trouble.
A Simple Handle
In order to avoid our memory-allocation problems, we shall define a handle class, which we shall name Shape, and ask our users to use objects of this class instead of Shape_base pointers. In other words, instead of writing:
Shape_base* p = new Circle;
our users should write:
Shape s(new Circle);
and instead of calling p->draw, they should call s.draw. Deleting the object s will delete the shape to which it points as well.
We now know enough to start defining our handle class:
class Shape { public: Shape(Shape_base* p0): p(p0) { } ~Shape() { delete p; } void draw() const { p->draw(); } // other functions as needed Shape(const Shape&); Shape& operator=(const Shape&); private: Shape_base* p; };
Our constructor and destructor are straightforward: the constructor accepts a pointer from our user and remembers it; the destructor deletes the object to which our pointer points. The draw function is similarly straightforward: it merely calls the draw function associated with the object to which were referring.
Unfortunately, were not done we havent yet written our copy constructor or assignment operator. Whenever a class needs a destructor, you can be pretty certain that it will also need a copy constructor and an assignment operator.
The Copy Constructor
What should copying a Shape do? On the surface, the answer is trivial: because we decided that every Shape would correspond to a single object with a type derived from Shape_base, copying Shape must copy the corresponding object. Indeed, doing so must allocate a new object of the same type in order to make the copy.
In order to do so, lets give our inheritance hierarchy a new virtual function named clone:
class Shape_base { public: virtual void draw() const = 0; // added virtual Shape_base* clone() const = 0; virtual ~Shape_base(); }; class Circle: public Shape_base { public: virtual void draw() const; // added virtual Shape_base* clone() const; private: double diameter; };
The idea is that if we call the clone member of any shape, it will construct a new object of the same type that is a copy of the original and return a pointer to the copy as a Shape_base*. Because the clone member is a pure virtual, every one of the derived classes will have to define it.
Once we have our clone function, the Shape copy constructor is not hard to define:
Shape::Shape(const Shape& s): p<BR> (s.p->clone()) { }
We copy a Shape by asking the object to which the Shape points to clone itself.
Suppose, for example, that we allocate a Circle and bind a Shape to it:
Shape s = new Circle;
Then we have the following situation:
If we now form a copy of Shape, doing so will call the Circle objects clone member, which will create a new Circle object bound to the copy of the Shape. For example, if we execute:
Shape s1 = s;
the situation will look like this:
Now we can rewrite our examples to use Shape objects instead of Shape_base* pointers. We begin by creating our shapes vector again:
std::vector<Shape> shapes; shapes.push_back(new Circle); shapes.push_back(new Polygon); // ... for (std::vector<Shape>::const_iterator s = shapes.begin(); s != shapes.end(); ++s) { s->draw(); }
Yes, we mean s->draw(), not (*s)->draw() or s.draw(). The reason is that s is still an iterator, so *s is still the object to which that iterator refers, but now that object is a Shape rather than a pointer. If you look back at the definition of the Shape class, you will see that it has a draw function that calls the Shape_base class draw function.
Now lets select some of our shapes again and put them in some_shapes:
std::vector<Shape> some_shapes; for (std::vector<Shape>::const_iterator s = shapes.begin(); s != shapes.end(); ++s) { if (condition(s)) some_shapes.push_back(*s); }
Look at that call to:
some_shapes.push_back(*s);
Because some_shapes is a vector<Shape> and *s is now a Shape, this call is copying a Shape object. Accordingly, it is also copying (via the clone function) the object to which that Shape refers. It has no choice but to do so, because of our decision that each Shape object refers to a unique object from the Shape_base hierarchy.
After executing this code, the situation looks like this:
By copying the Polygon, we have completely gotten rid of the allocation problems we had before: deallocating either of the shapes or some_shapes vectors will have no effect on the other vector, as they no longer have any objects in common. The key to being able to avoid the allocation problems is our handle class, which ensures that users will not forget to copy shapes when they copy the pointers to them.
Exception Safety
So far, we have not considered what happens if one of the functions that we have written throws an exception [5]. Fortunately, the answer turns out to be not much. In general, exceptions cause trouble only if they occur after the programs state has changed in some way, either by allocating a resource or modifying a data structure. An exception that occurs before the first state change is harmless, because when the exception is handled, the state will be as if nothing had happened. Therefore, to convince ourselves that the program so far is exception safe, we need to look only at places where the state changes.
There arent many such places. We assume that the draw function doesnt change the state, and that destructors dont throw exceptions. Except for the copy constructor, our constructors dont do anything that might throw an exception; allocating the Shape_base objects in the first place is our users responsibility.
The only potential problem, then, is the copy constructor and the clone function that it calls. Even here, there should be no problem because calling the clone function is the first thing the copy constructor does. Therefore, to ensure exception safety, all we need to assume is that if the clone function throws an exception, it first cleans up any resources that it might have allocated. Again, that responsibility is part of writing the clone function; if that function is exception safe, our code so far will not cause any new problems with exceptions.
Assignment
We have only one problem left to solve, namely defining our assignment operator. We didnt need to use the assignment operator in this article, but of course we need to define it if we want our Shape class to be generally useful. Curiously enough, it is here that we have to work the hardest to ensure exception safety.
We have lost count of how many times we have seen assignment operators defined along the following lines. Sometimes, we have even defined them this way ourselves:
// What's wrong with this code? Shape& Shape::operator=(const Shape& source) { if (this != &source) { delete p; p = source->clone(); } return *this; }
Most C++ programmers these days are aware of the need to ensure that an assignment operator works correctly when we use it to assign an object to itself. For example, suppose we have a Shape named s that happens to refer to a Circle:
and we execute:
s = s;
Then, within the body of the assignment operator, this will point to the same object to which source refers. If we did not compare this with &source, we would first delete this->p, which would destroy the Circle object:
Then we would attempt to clone the object pointed to by source->p, which is the same object that we had just deleted. The result would be undefined behavior.
Therefore, we need a strategy that will ensure that the assignment works even if we are assigning an object to itself. However, this particular strategy still leaves us with an exception-safety problem.
The reason is that it makes two changes to the programs state: first it deallocates the object to which p points; then it allocates a new object. We assume that deleting an object will not throw an exception, but allocating the new object might do so. As we already noted, our trouble comes from the fact that we might have to deal with an exception after we have changed the programs state.
When we describe the problem that way, the solution is easy: rewrite the assignment operator so that it does not change the state until it is no longer possible for exceptions to be thrown. We can achieve that state of affairs by calling clone first because thats where the exception might be and only then deleting p. Of course, if we follow that strategy, we need a place to save the result of calling clone:
// This one is much better 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; }
We have rewritten our assignment operator so that the first state change happens only after the call to clone a point at which no future exceptions are possible. Accordingly, this version of the assignment operator is exception safe.
Discussion
Every C++ program that uses inheritance must manage memory somehow. The most obvious way to do so is directly, but programmers who create complicated data structures often have trouble figuring out what parts of those data structures are safe to delete when.
The classical method of dealing with such complexity is to hide it in a class. Such classes are typically called handles; the idea is to attach a handle object to another object that contains the actual data. The simplest form of a handle, which we have discussed in this article, is one in which each handle object corresponds to a single object from the inheritance hierarchy. Such handles are straightforward to use and to implement and tend to be intrinsically exception safe in almost all respects.
The one exception hazard in such a class is typically the assignment operator. Assignment operators often test for self-assignment to avoid aliasing problems. As Herb Sutter has observed in [6], programs that need such tests are almost always exception unsafe.
By rewriting the assignment operator, we ensure that we do not do anything irrevocable until the possibility of throwing an exception has passed. This strategy ensures that if an exception occurs while our assignment operator is executing, we do not corrupt the rest of our system.
Our Shape class still has a significant shortcoming: when we copy Shape objects, as we did to create the some_shapes vector, we create what might be unnecessary copies of the objects to which the Shape objects refer. In our next article, we will modify our handle to deal with this problem and see how that modification affects our efforts to maintain exception safety.
Notes
[1] A. R. Koenig. Variations on a Handle Theme, Journal of Object-Oriented Programming, 8(6).
[2] A. R. Koenig. Another Handle Variation, Journal of Object-Oriented Programming, 8(7).
[3] A. R. Koenig and B. E. Moo. Ruminations on C++ (Addison-Wesley, 1997).
[4] A. R. Koenig and B. E. Moo. Accelerated C++ (Addison-Wesley, 2000).
[5] H. Sutter. Exceptional C++ (Addison-Wesley, 2000).
[6] B. Stroustrup. The C++ Programming Language, Third Edition (Addison-Wesley, 1997). Appendix E discusses exception safety in detail.
Andrew Koenig is a member of the Large-Scale Programming 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++.
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 lectures worldwide.