Effective C++ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GameCharacter {
public:
virtual int healthValue() const {
// default implementation
}
...
};
class Monster: public GameCharacter {
public:
virtual int healthValue() const {
// custom implementation
}
...
}

This is the most common design, but not the only one. What’s alternative to this approach?

Non-virtual Interface (NVI) Idiom (also called Template Method Pattern)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GameCharacter {
public:
int healthValue() const
{
// pre-condition
int retVal = doHealthValue();
// post-condition
return retVal;
}
...
private:
virtual int doHealthValue() const
{
// default implementation
}
};
class Monster: public GameCharacter {
private:
virtual int doHealthValue() const {
// custom implementation
}
...
}

You may wonder first of all how can derived doHealthValue be called in base class? You need to understand access level is a compile-time concept. The runtime doesn’t know if a method was declared private or public. Calling derived doHealthValue in base class is totally valid because it’s called through runtime dynamic dispatching.
Note that healthValue is non-virtual in base class, and actual interface doHealthValue is private. You can read details of NIV here

THE STRATEGY PATTERN VIA FUNCTION POINTERS

You can argue that calculate health value is independent of characters, so it should not belong to GameCharacter class. GameCharacter class can take in a function pointer to calculate it’s health value. This is strategy pattern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GameCharacter; // forward declaration
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;
};

The beauty of this design is that it allows you to associate different health calculation method to the same class instances.

1
2
3
4
5
6
7
8
9
10
11
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{ ... }
...
};
int loseHealthQuickly(const GameCharacter&){ ... }
int loseHealthSlowly(const GameCharacter&){ ... }
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);

THE STRATEGY PATTERN VIA STD::FUNCTION

With slightly change from previous example, we can make the design much more flexible in terms of what form should health calculation function conform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...

private:
HealthCalcFunc healthFunc;
};

This way, GameCharacter can support much more rich way of defining health calculation function. In fact, it doesn’t have to be a function at all.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// non-int return type
short calcHealth(const GameCharacter&);
// functor
struct HealthCalculator {
int operator(){ ... }
};
// member function
class GameLevel {
public:
float health(const GameCharacter&) const;
...
};

class EvilBadGuy: public GameCharacter {
...
};

EvilBadGuy a1(calcHealth);
EvilBadGuy a2(HealthCalculator());

GameLevel currentLevel;
EvilBadGuy a3(
std::bind(&GameLevel::health,
currentLevel,
_1)
);

THE “CLASSIC” STRATEGY PATTERN

Instead of providing an implementation of health calculation via function or callable, the classic strategy pattern is to define hierarchal structure for health calculation methods too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
};

Reference:
“Effective C++” Third Edition by Scott Meyers.