Charlie Havener is a software engineer at Epsilon Data Management in Burlington, MA, where he is developing a custom query language compiler in C++ for the CM5 Connection machine super computer. He holds two masters degrees in EE and CS and teaches an advanced C++ lab course in the Northeastern University State-of-the-Art program. He may be reached on the Internet, cdh@world.std.com or cdh@epsilon.com
Introduction
In this very special frog pond you will encounter objects that change. A Tadpole will change into a Bullfrog even as an external pointer to it on a linked list does not change. The implementation technique that permits this bit of magic to happen is the powerful C++ "envelope and letter class" idiom. You can find it presented by James Coplien in his book Advanced C++ (Coplien 1992). To construct the various kinds of creatures that are the letter classes, you need a virtual constructor in the Creature base class.You can implement this special kind of constructor by making use of an enumeration type, as in:
enum type{ BULLFROG, TADPOLE, ...};But that is rather unsatisfying. The job can be better accomplished using a variation on the clone technology presented by Tom Cargill in his article "Virtual Constructors Revisited" (Cargill, Sept. 1992).Here there be magic. Watch carefully and see if you can keep your wits, sanity, and sense of humor intact as we all go for a swim in the object-oriented frog pond.
A Boring Little Frog Pond
Figure 1 shows a first attempt at a class hierarchy to represent various creatures in a pond. I will concentrate on frogs, but you can imagine extending it to different kinds of fish, bugs, and other slimy things. Listing 1 shows how a Pond can be instantiated and populated with creatures.The Pond class consists of an embedded singly-linked list of pointers to creatures as the only data object (see Figure 2) , and two member functions. The Pond::insert function sticks freshly created creatures into the linked list by calling the list's insert function. I have chosen to use the generic singly-linked list from the Rogue Wave tools.h++ library, but anything you have available will do. This list just contains pointers to the objects it controls. It does not "own" the objects themselves just because you inserted them. You still own them.
The Pond::activate function is a bit trickier. You can imagine the creatures having several member functions, like move and soundoff, that have the same signature. They take no arguments and return nothing. Then, using the weird but useful C++ pointer-to-member syntax, you can bind a pointer to a virtual move or soundoff function in the Creature base class. You pass this pointer as an argument to the activate member function of Pond.
The activate function iterates over the list, using the generic singly-linked list iterator in the case of Rogue Wave. That's how you broadcast the desired function activation request to all creatures in the pond. Simple huh?
It is easy to imagine storing lots of additional information about the frogs in the Frog base class. Some examples are gender, color, and age. But what about age? Remember, as tadpoles age they turn into frogs.
Changing an Object in Situ
Say the Creature base class contained a pointer to a creature as a data member and Creature::move forwarded the move operation through this pointer. Then you could alter the pointer somehow and thus effectively change the body of the creature at runtime.This interesting technique is called the "envelope and letter class" idiom (depicted in Figure 3) , as I mentioned earlier. The "letter" is one of a set of classes publicly derived from the base or "envelope" class. The outside world accesses the letter only through the envelope. Listing 2 shows how the Creature base class has been fleshed out with a data member that points to a creature. The virtual move member function merely passes the work along to the letter object through the pointer.
Note that the constructor now takes an enumerated type as one of its arguments. Using an enumerated type is not considered good style. It means the base class must know about all of its derived children and must be modified any time a new derived class is invented. To add different kinds of fish, you have to alter the base class enumerated type. You also have to alter the so-called virtual constructor for class Creature. It has an ugly case statement, the goto of object-oriented programming.
Another interesting new aspect of Listing 2 is the protected constructor in class Creature. This or some similar mechanism is needed to initialize the Creature class when it is part of the letter class. That is, when a Frog is instantiated, some constructor for the base class Creature must be invoked. In this example you don't need a default constructor for Creature. It is not meant to be instantiated by itself. Bruce Eckel shows a good approach in his article "Virtual Constructors" (Eckel Mar/Apr 1992). A protected default constructor does the job nicely.
Each Creature now has an envelope with the object pointer set to the letter, and a letter with its object pointer set to zero. This approach wastes some space. Attempts to reuse data that is common to the envelope and letter by placing it in a union requires the utmost care. If we needed a default constructor in the base class, then we just make a constructor in the base Creature class that takes a funny argument such as a class Noop{}. (See Coplien's polymorphic Number example in Advanced C++.)
The constructors, especially the copy constructor, of the derived classes must be sure to invoke the special base constructor or there can be infinite recursion. Clearly Listing 2 is less than perfect. It shares the defect with Listing 1 of using plain old pointer to char instead of a const pointer to char. Furthermore, copy constructors are needed everywhere or utter failure at runtime is guaranteed. And we have not yet done any object changing in place. So let's move on.
A Complete Solution
Listing 3 combines everything I have discussed, together with an extension to the clone technology presented by Cargill in "Virtual Constructors Revisited" to eliminate the need for an enumerated type and a case statement. Cargill optimizes away the very need for an envelope and letter class, but here I want to keep it. So the clone techniques have to be carefully applied.Careful attention has been paid to getting all the copy constructors properly implemented, an essential part of success. Not to beat a dead horse, but copy constructors are frequently left out when the class has either no data or only simple predefined types such as integers. However, when inheritance is involved, as here, explicit copy constructors are needed to invoke the base-class copy constructors. Even Coplien accidently left out the copy constructors from an "envelope and letter" example in the first edition of Advanced C++. Let's consider how things change in place before looking closer at the clone technique.
The first thing to notice is that the Creature base class has sprouted quite a few additional functions and a static data member, altered, which is also a pointer to Creature. This pointer is needed so the move member function in the envelope Creature base class can test it after the letter class move is called.
If the letter class has decided to change itself into another object, it must communicate this desire to the envelope in some way. It can't do so itself. That is like pushing on a string. The letter class has absolutely no way of communicating to the envelope except through a return value from a function, such as move, or through a global variable. The static Creature variable altered is a restricted global variable. It serves the purpose so long as the class doesn't have to be re-entrant. It must be static or the letter class would just be setting the variable called altered in its own base and not in the envelope base. Remember, there are two copies of all the data in the Creature base class.
Rather than do anything complicated for this example in terms of time and ageing, the Tadpole::move function just increments the age each time it is called. It makes a new Bullfrog when the time comes and sets the static class variable altered for use by the envelope class. The move member function in the envelope class tests altered. If the variable is non-zero, the function deletes the existing pointer to Creature, Tadpole for example, and replaces it with altered, which in this case is a pointer to Bullfrog. Finally the function resets altered to zero.
The clone technique uses a static class member newCreature(const Creature&) of class Creature to make new creatures. Every class now has a clone member function which returns a pointer to Creature. These clone functions return pointers to objects that will be letter objects.
The static newCreature(Creature &) function requires a Creature constructor that takes a pointer to a creature as its argument. This is absolutely essential because the newCreature function must return a pointer to an envelope Creature, not a letter Creature. If you think you've been having a bad day, just try debugging an envelope/letter program when you accidently pass around pointers to letter objects instead of envelope objects! Recall that the Creature pointer in the letter object is set to zero. Trying to pass work off through a null pointer does bad things. C++ does an outstanding job of finding bugs at compile time by enforcing type checks. However, the envelope/letter idiom moves a program into the realm of runtime type modification. It is much easier to get in trouble.
The main program creates new creatures of any kind by invoking the static Creature::newCreature function and passing it a reference to some creature to be cloned. If the kind of creature you want is not already extant, make an anonymous one, as shown in Listing 3, which the compiler will arrange to delete at its convenience. Presumably, it would be desirable to modify the attributes of the new clone using various member functions in a straightforward fashion. This example merely maintains the name.
Conclusion
You have seen a cute but usable example of the "envelope and letter class" idiom. You have also seen how it can be combined with clone technology to provide an extendable framework for situations that require objects that change in place at runtime. It's not an easy idiom to understand or to get right. But once you master it, you will find it to be a powerful addition to your kit of C++ tools.(C) 1993 Charles Havener
References
Cargill, Tom. September 1992. "Virtual Constructors Revisited," C++ Report.Coplien, James O. 1992. Advanced C++. Reading, MA: Addison-Wesley.
Eckel, Bruce. March/April 1992. "Virtual Constructors," C++ Report.