Item 35: Consider alternatives to virtual functions


So you're working on a video game, and you're designing a hierarchy for characters in the game. Your game being of the slash-and-burn variety, it's not uncommon for characters to be injured or otherwise in a reduced state of health. You therefore decide to offer a member function, healthValue, that returns an integer indicating how healthy the character is. Because different characters may calculate their health in different ways, declaring healthValue virtual seems the obvious way to design things:

 class GameCharacter { public:   virtual int healthValue() const;        // return character's health rating;   ...                                     // derived classes may redefine this }; 

The fact that healthValue isn't declared pure virtual suggests that there is a default algorithm for calculating health (see Item 34).

This is, indeed, the obvious way to design things, and in some sense, that's its weakness. Because this design is so obvious, you may not give adequate consideration to its alternatives. In the interest of helping you escape the ruts in the road of object-oriented design, let's consider some other ways to approach this problem.

The Template Method Pattern via the Non-Virtual Interface Idiom

We'll begin with an interesting school of thought that argues that virtual functions should almost always be private. Adherents to this school would suggest that a better design would retain healthValue as a public member function but make it non-virtual and have it call a private virtual function to do the real work, say, doHealthValue:

 class GameCharacter { public:   int healthValue() const               // derived classes do not redefine   {                                     // this   see Item 36     ...                                 // do "before" stuff   see below     int retVal = doHealthValue();       // do the real work     ...                                 // do "after" stuff   see below     return retVal;   }   ... private:   virtual int doHealthValue() const     // derived classes may redefine this   {     ...                                 // default algorithm for calculating   }                                     // character's health }; 

In this code (and for the rest of this Item), I'm showing the bodies of member functions in class definitions. As Item 30 explains, that implicitly declares them inline. I'm showing the code this way only to make it easier to see what is going on. The designs I'm describing are independent of inlining decisions, so don't think it's meaningful that the member functions are defined inside classes. It's not.

This basic design having clients call private virtual functions indirectly through public non-virtual member functions is known as the non-virtual interface (NVI) idiom. It's a particular manifestation of the more general design pattern called Template Method (a pattern that, unfortunately, has nothing to do with C++ templates). I call the non-virtual function (e.g., healthValue) the virtual function's wrapper.

An advantage of the NVI idiom is suggested by the "do 'before' stuff" and "do 'after' stuff" comments in the code. Those comments identify code segments guaranteed to be called before and after the virtual function that does the real work. This means that the wrapper ensures that before a virtual function is called, the proper context is set up, and after the call is over, the context is cleaned up. For example, the "before" stuff could include locking a mutex, making a log entry, verifying that class invariants and function preconditions are satisfied, etc. The "after" stuff could include unlocking a mutex, verifying function postconditions, reverifying class invariants, etc. There's not really any good way to do that if you let clients call virtual functions directly.

It may have crossed your mind that the NVI idiom involves derived classes redefining private virtual functions redefining functions they can't call! There's no design contradiction here. Redefining a virtual function specifies how something is to be done. Calling a virtual function specifies when it will be done. These concerns are independent. The NVI idiom allows derived classes to redefine a virtual function, thus giving them control over how functionality is implemented, but the base class reserves for itself the right to say when the function will be called. It may seem odd at first, but C++'s rule that derived classes may redefine private inherited virtual functions is perfectly sensible.

Under the NVI idiom, it's not strictly necessary that the virtual functions be private. In some class hierarchies, derived class implementations of a virtual function are expected to invoke their base class counterparts (e.g., the example on page 120), and for such calls to be legal, the virtuals must be protected, not private. Sometimes a virtual function even has to be public (e.g., destructors in polymorphic base classes see Item 7), but then the NVI idiom can't really be applied.

The Strategy Pattern via Function Pointers

The NVI idiom is an interesting alternative to public virtual functions, but from a design point of view, it's little more than window dressing. After all, we're still using virtual functions to calculate each character's health. A more dramatic design assertion would be to say that calculating a character's health is independent of the character's type that such calculations need not be part of the character at all. For example, we could require that each character's constructor be passed a pointer to a health calculation function, and we could call that function to do the actual calculation:

 class GameCharacter;                               // forward declaration // function for the default health calculation algorithm int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public:   typedef int (*HealthCalcFunc)(const GameCharacter&);   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)   : healthFunc(hcf)   {}   int healthValue() const   { return healthFunc(*this); }   ... private:   HealthCalcFunc healthFunc; }; 

This approach is a simple application of another common design pattern, Strategy. Compared to approaches based on virtual functions in the GameCharacter hierarchy, it offers some interesting flexibility:

  • Different instances of the same character type can have different health calculation functions. For example:

     class EvilBadGuy: public GameCharacter { public:   explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)   : GameCharacter(hcf)   { ... }   ... }; int loseHealthQuickly(const GameCharacter&);    // health calculation int loseHealthSlowly(const GameCharacter&);     // funcs with different                                                 // behavior EvilBadGuy ebg1(loseHealthQuickly);             // same-type charac- EvilBadGuy ebg2(loseHealthSlowly);              // ters with different                                                 // health-related                                                 // behavior 

  • Health calculation functions for a particular character may be changed at runtime. For example, GameCharacter might offer a member function, setHealthCalculator, that allowed replacement of the current health calculation function.

On the other hand, the fact that the health calculation function is no longer a member function of the GameCharacter hierarchy means that it has no special access to the internal parts of the object whose health it's calculating. For example, defaultHealthCalc has no access to the non-public parts of EvilBadGuy. If a character's health can be calculated based purely on information available through the character's public interface, this is not a problem, but if accurate health calculation requires non-public information, it is. In fact, it's a potential issue anytime you replace functionality inside a class (e.g., via a member function) with equivalent functionality outside the class (e.g., via a non-member non-friend function or via a non-friend member function of another class). This issue will persist for the remainder of this Item, because all the other design alternatives we're going to consider involve the use of functions outside the GameCharacter hierarchy.

As a general rule, the only way to resolve the need for non-member functions to have access to non-public parts of a class is to weaken the class's encapsulation. For example, the class might declare the non-member functions to be friends, or it might offer public accessor functions for parts of its implementation it would otherwise prefer to keep hidden. Whether the advantages of using a function pointer instead of a virtual function (e.g., the ability to have per-object health calculation functions and the ability to change such functions at runtime) offset the possible need to decrease GameCharacter's encapsulation is something you must decide on a design-by-design basis.

The Strategy Pattern via tr1::function

Once you accustom yourself to templates and their use of implicit interfaces (see Item 41), the function-pointer-based approach looks rather rigid. Why must the health calculator be a function instead of simply something that acts like a function (e.g., a function object)? If it must be a function, why can't it be a member function? And why must it return an int instead of any type convertible to an int?

These constraints evaporate if we replace the use of a function pointer (such as healthFunc) with an object of type TR1::function. As Item 54 explains, such objects may hold any callable entity (i.e., function pointer, function object, or member function pointer) whose signature is compatible with what is expected. Here's the design we just saw, this time using tr1::function:

 class GameCharacter;                                 // as before int defaultHealthCalc(const GameCharacter& gc);      // as before class GameCharacter { public:    // HealthCalcFunc is any callable entity that can be called with    // anything compatible with a GameCharacter and that returns anything    // compatible with an int; see below for details    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)    : healthFunc(hcf)    {}    int healthValue() const    { return healthFunc(*this);   }    ... private:   HealthCalcFunc healthFunc; }; 

As you can see, HealthCalcFunc is a typedef for a TR1::function instantiation. That means it acts like a generalized function pointer type. Look closely at what HealthCalcFunc is a typedef for:

 std::tr1::function<int (const GameCharacter&)> 

Here I've highlighted the "target signature" of this tr1::function instantiation. That target signature is "function taking a reference to a const GameCharacter and returning an int." An object of this tr1::function type (i.e., of type HealthCalcFunc) may hold any callable entity compatible with the target signature. To be compatible means that the entity's parameter can be implicitly converted to a const GameCharacter& and its return type can be implicitly converted to an int.

Compared to the last design we saw (where GameCharacter held a pointer to a function), this design is almost the same. The only difference is that GameCharacter now holds a tr1::function object a generalized pointer to a function. This change is so small, I'd call it inconsequential, except that a consequence is that clients now have staggeringly more flexibility in specifying health calculation functions:

 short calcHealth(const GameCharacter&);          // health calculation                                                  // function; note                                                  // non-int return type struct HealthCalculator {                        // class for health   int operator()(const GameCharacter&) const     // calculation function   { ... }                                        // objects }; class GameLevel { public:   float health(const GameCharacter&) const;      // health calculation   ...                                            // mem function; note };                                               // non-int return type class EvilBadGuy: public GameCharacter {         // as before   ... }; class EyeCandyCharacter:   public GameCharacter {  // another character   ...                                              // type; assume same };                                                 // constructor as                                                    // EvilBadGuy EvilBadGuy ebg1(calcHealth);                       // character using a                                                    // health calculation                                                    // function EyeCandyCharacter ecc1(HealthCalculator());        // character using a                                                    // health calculation                                                    // function object GameLevel currentLevel; ... EvilBadGuy ebg2(                                   // character using a   std::tr1::bind(&GameLevel::health,               // health calculation           currentLevel,                            // member function;           _1)                                      // see below for details ); 

Personally, I find what tr1::function lets you do so amazing, it makes me tingle all over. If you're not tingling, it may be because you're staring at the definition of ebg2 and wondering what's going on with the call to tr1::bind. Kindly allow me to explain.

We want to say that to calculate ebg2's health rating, the health member function in the GameLevel class should be used. Now, GameLevel::health is a function that is declared to take one parameter (a reference to a GameCharacter), but it really takes two, because it also gets an implicit GameLevel parameter the one this points to. Health calculation functions for GameCharacters, however, take a single parameter: the GameCharacter whose health is to be calculated. If we're to use GameLevel::health for ebg2's health calculation, we have to somehow "adapt" it so that instead of taking two parameters (a GameCharacter and a GameLevel), it takes only one (a GameCharacter). In this example, we always want to use currentLevel as the GameLevel object for ebg2's health calculation, so we "bind" currentLevel as the GameLevel object to be used each time GameLevel::health is called to calculate ebg2's health. That's what the tr1::bind call does: it specifies that ebg2's health calculation function should always use currentLevel as the GameLevel object.

I'm skipping over a host of details, such as why "_1" means "use currentLevel as the GameLevel object when calling GameLevel::health for ebg2." Such details wouldn't be terribly illuminating, and they'd distract from the fundamental point I want to make: by using tr1::function instead of a function pointer, we're allowing clients to use any compatible callable entity when calculating a character's health. Is that cool or what?

The "Classic" Strategy Pattern

If you're more into design patterns than C++ coolness, a more conventional approach to Strategy would be to make the health-calculation function a virtual member function of a separate health-calculation hierarchy. The resulting hierarchy design would look like this:

If you're not up on your UML notation, this just says that GameCharacter is the root of an inheritance hierarchy where EvilBadGuy and EyeCandyCharacter are derived classes; HealthCalcFunc is the root of an inheritance hierarchy with derived classes SlowHealthLoser and FastHealthLoser; and each object of type GameCharacter contains a pointer to an object from the HealthCalcFunc hierarchy.

Here's the corresponding code skeleton:

 class GameCharacter;                            // forward declaration class HealthCalcFunc { public:   ...   virtual int calc(const GameCharacter& gc) const   { ... }   ... }; HealthCalcFunc defaultHealthCalc; class GameCharacter { public:   explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)   : pHealthCalc(phcf)   {}   int healthValue() const   { return pHealthCalc->calc(*this);}   ... private:   HealthCalcFunc *pHealthCalc; }; 

This approach has the appeal of being quickly recognizable to people familiar with the "standard" Strategy pattern implementation, plus it offers the possibility that an existing health calculation algorithm can be tweaked by adding a derived class to the HealthCalcFunc hierarchy.

Summary

The fundamental advice of this Item is to consider alternatives to virtual functions when searching for a design for the problem you're trying to solve. Here's a quick recap the alternatives we examined:

  • Use the non-virtual interface idiom (NVI idiom), a form of the Template Method design pattern that wraps public non-virtual member functions around less accessible virtual functions.

  • Replace virtual functions with function pointer data members, a stripped-down manifestation of the Strategy design pattern.

  • Replace virtual functions with tr1::function data members, thus allowing use of any callable entity with a signature compatible with what you need. This, too, is a form of the Strategy design pattern.

  • Replace virtual functions in one hierarchy with virtual functions in another hierarchy. This is the conventional implementation of the Strategy design pattern.

This isn't an exhaustive list of design alternatives to virtual functions, but it should be enough to convince you that there are alternatives. Furthermore, their comparative advantages and disadvantages should make clear that you should consider them.

To avoid getting stuck in the ruts of the road of object-oriented design, give the wheel a good jerk from time to time. There are lots of other roads. It's worth taking the time to investigate them.

Things to Remember

  • Alternatives to virtual functions include the NVI idiom and various forms of the Strategy design pattern. The NVI idiom is itself an example of the Template Method design pattern.

  • A disadvantage of moving functionality from a member function to a function outside the class is that the non-member function lacks access to the class's non-public members.

  • tr1::function objects act like generalized function pointers. Such objects support all callable entities compatible with a given target signature.




Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
ISBN: 321334876
EAN: N/A
Year: 2006
Pages: 102

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net