A virtual function call is termed a “message dispatch.” A call that acts virtual on multiple parameters is called multiple dispatch, which is not directly supported in C++. Several resolutions exist, but none is without its disadvantages.
For example, considering we are writing a video game involving space ships, space stations, and asteroids:
|
|
When one GameObject
collides with another, we call a function depending on both their dynamic types:
|
|
Now comes the double dispatch: since collisions betwenn different GameObject
effects the environment differently, we want processCollision
to act virtual on both object1
and object2
, but C++ only offers virtual support for one parameter, like object1.processCollision(object2)
. We must come up with some approaches ourselves instead of relyin on compilers.
Using Virtual Function and RTTI
We need double dispatch, so we can use virtual functions for first half of what we need, and use chains of if-then-else
s for rest half:
|
|
|
|
The danger in this design: each collide
function must be aware of each of its sibling classes, so if a new type of object is added to the game, we must update each RTTI-based if-then-else
chain in the proram that might encounter the new object, which in essence is unmaintainable in the long run. We added a final else
clause where control winds up if we hit an unnknown object, throwing an exception to callers in the hope that they handle the error better than we can (but since we are running into something we didn’t know existed, they almost can’t do anything more satisfying.)
Using Virtual Functions Only
We can also implement double-dispatching as two separate virtual function calls:
|
|
|
|
Note that this implementation is not a resursive call: being inside a member function of the class Spaceship
, the static type of *this
is of type SpaceShip
, so the call is routed to the collide(SpaceShip&)
instead of going back to collide(GameObject&)
. All the collide
functions are virtual, so finally this call will resolve to the implementation of collide
corresponding to the real type of otherObject
, where both types are known. So the implementation is simply:
|
|
There’s no RTTI, no need to throw unexpected object types. Still, there’s the same old flaw: each class must know about its siblings. Even worth, the way in which the code must be updated in the case of adding new classes is difficult to extend: if we add a new class, say class Satellite: public GameObject
, to our game, we’d have to add a new collide
function to each of the existing classes in the program, rather than just another else
clause in one function before.
Now let’s do a small sum-up:
- Virtual function approach is safer than the RTTI strategy, but it constrains the extensibility of the system to match that of our ability to edit header files
- RTTI makes no recompilation demands, but it generally leads unmaintainable software.
- The best recourse is to modify the design to eliminate the need for double-dispatching.
Emulating Virtual Function Tables
Actually, we can build our own virtual function tables, similar to how compilers implement virtual functions by creating an array of function pointers (the vtbl) and then indexing into that array when a virtual function is called, except that this customized version support double-dispatching. Moreover, the virtual function tables is more efficient than the RTTI-based code (indexing into an array and following a function pointer vs running through a series of if-then-else
tests), and we isolate the use of RTTI to a single location where the array of function pointers is initialized.
|
|
|
|
Note that there’s another design which seems doable but is actually error-prone:
|
|
Here we tell the compiler that hitSpaceShip
, hitSpaceStation
, and hitAsteroid
are functions expecting a GameObject
argument, which is not true. hitSpaceShip
expects a SpaceShip
, hitSpaceStation
expects a SpaceStation
, and hitAsteroid
expects an Asteroid
. All these functions are declared as pass-by-reference, which in fact is implemented by passing a pointer to an object, so ideally compilers will pass the declared type of the parameter(say hitSpaceShip
for function hitSpaceShip
) in the function being called. However, due to the object layout of classes under an inheritance path, after the cast above, it is possible that the wrong address (say GameObject
) is passed into the function, because inside a SpaceShip
object there are both a derived class part as well as a base class part, each having a different address (for detailed discussion, refer to More Effective C++ Item 31-Initializing Emulated Virtual Function Talbes).
Using Non-Member Collision-Processing Functions
Still, similar to pure virtual functions based approach, there is one remaining problem: because the associative array contains pointers to member functions, once a new type of GameObject
is added to the game, every class definition needs to be updated and recompiled, even if that class does not care about the new type of object.
To solve this problem, we change the associative array so that it contains pointers to non-member functions. This update also helps us address a design question before: a collision between objects of types A and B should be handles by neither A nor B (depending on whichever is the left-hand argument to processCollision
) but instead in some neutral location outside both classes.
|
|
Note that makeStringPair
, initializeColllisionMap
, and lookup
were declared inside an unnamed namespace, therefore each must also be implemented within the same namespace.
Now we’ve solved the problem: if there are new classes to the hierarchy, it requires only the addition of more map insertions in initializeCollisionMap
and the declarations of the new collision-processing functions in the unnamed namespace associated with the implementation of processCollision
.
However, as uaual, there’s still a disadvantage in this design: these non-member functions will not support inheritance-based parameter conversions such as how double-virtual-function-call mechanism does.
For example, suppose the concrete classes CommercialShip
and MilitaryShip
inherit from the newly abstract class SpaceShip
(according to the guidance of MECpp item 33). If a MilitaryShip
object and an Asteroid
collided, we’d expect void shipAsteroid(GameObject&, GameObject&)
to be called. However, in fact, an UnknownCollision
would be thrown, because lookup
would be asked to find a function corresponding to the type names MilitaryShip
and Asteroid
, and no such function would be found in collisionMap
- after all, lookup
has no way of knowing that MilitaryShip
can be treated like a SpaceShip
.
Modifying Emulated Virtual Function Talbes Dynamically
If we’d like to add, remove, or change collision-processing functions as the game proceeds, the static collisionMap
will not meet this requirement. In this case, we can turn the concept of a static map into a class that offers member functions allowing us to modify the contents of the map dynamically:
|
|
Clients wishing to add an entry to the map simply do the following step:
|
|
We must take care to ensure that these map entries are added to the map before any collisions occurs. One way is to have constructors in GameObject
subclasses check to make sure the appropriate mappings had been added each time an object was created, but then we have to pay a small performance penalty at runtime. An alternative would be to create a RegisterCollisionFunction
class:
|
|
Clients then use global objects of this type to automatically register the functions before main
is invoked to insure the map is initialized properly before any collision occurs:
|
|
If, later, a new derived class Satellite: public GameObject {...};
is added, and one or more new collision-processing functions are written (say, void satelliteShip(GameObject&, GameObject&)
, and void satelliteAsteroid(GameObject&,GameObject&)
, etc), we can simply add them to the map without disturbing existing code:
|
|
Overall, this makes it easy to provide data for a map-based implementation, but it doesn’t change the fact that there’s no perfect way to implement multiple dispatch.