It is better to break a rule of thumb than to break your thumbs on a difficult problem.
Word has somehow got around that a split infinitive is always wrong. This is of a piece with the sentimental and outworn notion that it is always wrong to strike a lady. James Thurber, The Owl in the Attic and Other Perplexities
Break any of these rules sooner than say anything outright barbarous. George Orwell, Politics and the English Language
Ive always thought that both the curious observation from Mr. Thurber and the more staid advice from Mr. Orwell had application to object-oriented design. In earlier columns, I may have inadvertently given the impression that our C++ programming idioms are in some way a straightjacket that controls every aspect of the design process. Far from it. Properly used, idiom can simplify the process of design and communication of a design, leaving the designer free to expend creativity where its needed. However, there are times when use of even the most sensible and common programming idiom is inappropriate to the context of a design, and the designer is forced to depart from the standard approach to a problem.
In this installment, well examine two of our most cherished C++ programming idioms and show how departing from them in an intelligent and reasoned manner can result in a better design than we would have achieved by following them blindly.
Overloading Virtual Functions
In general, we avoid overloading virtual functions, because doing so is likely to defeat polymorphism. If we define polymorphism as the ability of an object to respond to several different interfaces, then we must ensure that an operation on the object exhibits the same behavior no matter what interface is used to access it. Consider the following base class fragment:
class Shape { public: //... virtual void rotate( int ); virtual void rotate( double ); };Consider a derived class produced by a designer who has determined that only the integer version of rotate requires different behavior in the derived class.
class Blob : public Shape { public: //... void rotate( int ); };Here we have an unhappy confluence of overloading and overriding which have nothing to do with each other. The behavior of a Blob object will vary depending on the interface used to access it.
Blob *blob = new Blob; Shape *shape = blob; shape->rotate( 12.3 ); // OK // oops! rotate(int)! blob->rotate( 12.3 );So, for well-behaved polymorphism, it is almost always a good idea to avoid overloading virtual functions.
Virtual Visiting
Almost always. Lets consider an instance of the Visitor [1] pattern for a hierarchy of financial instruments, where the hierarchy uses multiple inheritance to leverage subsystems written in terms of interface classes.
class Saveable { // persistent // interface virtual void save() = 0; // ... }; class Priceable { // pricing // interface virtual void price() = 0; // ... }; class Deal { public: virtual void accept( DealVisitor & ) = 0; virtual void validate() = 0; //... }; class Bond : public Deal, public Priceable { // Bonds can be priced public: void accept( DealVisitor & ); void validate(); void price(); //... }; class Swap : public Deal, public Priceable, public Saveable { // Swaps can be priced and saved public: void accept( DealVisitor & ); void validate(); void price(); void save(); //... }; class Eq : public Deal // cant do a thing with // equities lately... { /* ... */ };The use of a Visitor allows us to effectively add new operations to the Deal hierarchy without recompilation an important property if we dont have access to the source code and need to immediately perform some maintenance. If we follow our idiom above, well produce a DealVisitor interface with differently named functions for each deal type.
class DealVisitor { public: virtual void visitBond( Bond * ) = 0; virtual void visitSwap( Swap * ) = 0; virtual void visitEq( Eq * ) = 0; };The accept operation defined by each concrete Deal type must be written to specifically invoke the corresponding member of the DealVisitor.
void Swap::accept( DealVisitor &v ) { v.visitSwap( this ); } void Bond::accept( DealVisitor &v ) { v.visitBond( this ); } void Eq::accept( DealVisitor &v ) { v.visitEq( this ); }This allows us to easily add new operations to the hierarchy without recompiling it.
class ProcessDeal : public DealVisitor { void visitBond( Bond *d ) { d->validate(); d->price(); } void visitSwap( Swap *d ) { d->validate(); d->price(); d->save(); } void visitEq( Eq *d ) { d->validate(); } };However, this design has a problem. Weve employed a Visitor in the Deal hierarchy in order to be able to easily maintain the hierarchy without having access to the source. After all, if we were able to straightforwardly modify and recompile the Deal hierarchy, we would simply have added the ability to directly process Deals to the Deal classes themselves.
class Deal { public: virtual void accept( DealVisitor & ) = 0; virtual void validate() = 0; virtual void process() = 0; //... };What happens if we add a new class to the hierarchy and would like to process it? The Visitor pattern works tremendously well if the hierarchies it visits are stable, but breaks loudly when the structure of a visited hierarchy is modified. Generally, this loud breakage is advantageous, because it detects errors that other approaches, such as run-time type queries, tend to overlook. However, in this case we would like to easily add a new Deal type and process it without recompiling the existing source. The problem is that adding a new Deal class requires modification of the DealVisitor class, which in turn requires recompilation of the Deal hierarchy. No good.
Lets overload the virtual visit functions in the DealVisitor. Live a little.
class DealVisitor { public: virtual void visit( Deal * ); virtual void visit( Bond * ) = 0; virtual void visit( Swap * ) = 0; virtual void visit( Eq * ) = 0; };Here weve used the differing argument types of the visit members to distinguish the functions. One advantage of this approach is that the source code of the various deal accept functions is simplified.
void Swap::accept( DealVisitor &v ) { v.visit( this ); } void Swap::accept( DealVisitor &v ) { v.visit( this ); } void NewDealType::accept ( DealVisitor &v ) { v.visit( this ); }The accept operations for Swap, Bond, and Eq still invoke the appropriate type-specific visit operations. However, the use of overloading has allowed us to explicitly add a hook to the DealVisitor base class that can accept any type of Deal that is not handled by more specific visit operations. This means that our NewDealType can be visited by any DealVisitor and will be processed with whatever default behavior is provided in DealVisitor::visit(Deal*). In the future, if we are able to modify the DealVisitor hierarchy to add a visit(NewDealType *) to the interface, NewDealType::accept will be automatically modified on recompilation to invoke the more specific visit operation. No source code changes.
The real advantage of this approach is that it permits some seriously macho hacking (all in a good cause, of course) [2]. Given the presence of the catchall hook in the base class, we can derive a special-purpose DealVisitor that is aware of the existence of NewDealType.
class ProcessDealHack : public ProcessDeal { void visit( Deal *d ) { if( NewDealType *n = dynamic_cast<NewDealType *> (d) ) // process the NewDealType else // base processes // unrecognized deal types ProcessDeal::visit( d ); } };Whats this? you may be saying, Is this the guy whos been railing against overuse of run-time type information? Hey, read the title of the article. This is a (hopefully) temporary hack, and a very clever one. The appropriately-named ProcessDealHack hijacks ProcessDeal::visit(Deal *) to check for any new Deal types that may have come into existence since the compilation and release of the Deal hierarchy. This is only possible if we overload the virtual visit functions in the DealVisitor base class, in contradiction of the usual design idiom.
Note also that the major difficulty engendered by overloading virtual functions occurs when an object is accessed through both its base-class and derived-class interfaces. In the case of an instance of the Visitor pattern, the concrete derived visitor types are accessed only through the base class interface. In the code examples above, weve made this general policy explicit by overriding the public base class virtual functions with private derived class functions [3].
Copy Operation Implementation
One of the most common and useful C++ idioms is the copy operation idiom. Every abstract data type in C++ should make a decision about its copy assignment operator and copy constructor. Either the compiler should be allowed to write them, the programmer should write them, or they should be disallowed.
If the programmer writes these operations, we know exactly how they should be written. (However, the standard way of writing these operations has evolved over the years. This is one of the advantages of idiom over dictate; idiom evolves to suit the current context of use.)
class X { public: X( const X & ); X &operator =( const X & ); //... };While the C++ language permits a lot of leeway in their definition, it is almost invariably a good idea to actually declare these operations as they are above: both operations take a reference to a constant, and the copy assignment is non-virtual and returns a reference to a non-const. Clearly, neither of these operations will change its operand. It wouldnt make sense.
X a; X b( a ); // a won't change a = b; // b won't changeNon-Standard Copying
Except sometimes. The Standard C++ auto_ptr template has some very unusual requirements. It is a resource handle charged with cleaning up heap-allocated storage when it is no longer needed.
void f() { auto_ptr<Blob> blob( new Blob ); //... // automatic deletion of Blob }Fine, but what happens when the student interns are set loose on this code?
void g( auto_ptr<Blob> arg ) { //... // automatic deletion of Blob } void f() { auto_ptr<Blob> blob( new Blob ); g( blob ); // another deletion of Blob!!! }One approach might be to unambiguously disallow copy operations for auto_ptr, but that would severely limit their use and make impossible a number of useful auto_ptr idioms. Another approach might be to invisibly add a reference count to the auto_ptr, but that would increase the expense of employing a resource handle. The approach taken by the standard auto_ptr is to intentionally depart from the copy operation idiom.
template <class T> class auto_ptr { public: auto_ptr( auto_ptr & ); auto_ptr &operator =(auto_ptr &); //... private: T *object_; };(The standard auto_ptr also implements template member functions corresponding to these non-template copy operations, but the observations for those are similar.) Here the right-hand side of each operation is non-const! When one auto_ptr is initialized by or assigned to by another, the source of the initialization or assignment gives up ownership of the heap-allocated object to which it refers by setting its internal object pointer to null.
As is often the case when departing from idiom, there was some initial confusion about how auto_ptr should be properly used. However, this departure has allowed the development of a number of very productive new idioms centered around object ownership issues, and the use of auto_ptrs as sources and sinks of data looks to really be a very profitable new design area. In effect, a required departure from an existing, successful idiom has resulted in a family of new idioms.
Gotta Split
We could obviously examine many other idioms that have been successfully and properly circumvented. However, the lesson of our two examples above is clear. To slavishly follow idiom in the face of an obviously better design is often (as Winston Churchill was said to sardonically remark when an over-zealous editor criticized his ending a sentence with a preposition) an impertinence up with which [we should] not put. It is better in such situations to rationally follow the better design and boldly go where no programmer has gone before.
Notes and References
[1] Gamma et al. Design Patterns (Addison Wesley, 1995), p. 331.
[2] John Vlissides. Pattern Hatching (Addison Wesley, 1998), p. 79. I am not accusing the author of being macho, unless he would like to be so accused.
[3] Dont try this in Java. It wont work. This is, I think, an example of the danger of trying to encode good programming style in the programming language itself rather than in its idiomatic usage.
Stephen C. Dewhurst is the president of Semantics Consulting, Inc., located among the cranberry bogs of southeastern Massachusetts. He specializes in C++ and object-oriented mentoring and consulting, and training in advanced C++ programming, STL, and design patterns. He may be reached at <www.semantics.org>.