📄 mi31.htm
字号:
throw CollisionWithUnknownObject(otherObject);
}
}
</PRE>
</UL>
<P><A NAME="dingp19"></A><A NAME="34944"></A>
Notice how we need to determine the type of only one of the objects involved in the collision. The other object is <CODE>*this</CODE>, and its type is determined by the virtual function mechanism. We're inside a <CODE>SpaceShip</CODE> member function, so <CODE>*this</CODE> must be a <CODE>SpaceShip</CODE> object. Thus we only have to figure out the real type of <CODE>otherObject</CODE>.<SCRIPT>create_link(19);</SCRIPT>
</P>
<P><A NAME="dingp20"></A><A NAME="34945"></A>
There's nothing complicated about this code. It's easy to write. It's even easy to make work. That's one of the reasons RTTI is worrisome: it looks harmless. The true danger in this code is hinted at only by the final <CODE>else</CODE> clause and the exception that's thrown <NOBR>there.<SCRIPT>create_link(20);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp21"></A><A NAME="34946"></A>
<A NAME="p232"></A>We've pretty much bidden <I>adios</I> to encapsulation, because each <CODE>collide</CODE> function must be aware of each of its sibling classes, i.e., those classes that inherit from <CODE>GameObject</CODE>. In particular, if a new type of object — a new class — is added to the game, we must update each RTTI-based <CODE>if</CODE>-<CODE>then</CODE>-<CODE>else</CODE> chain in the program that might encounter the new object type. If we forget even a single one, the program will have a bug, and the bug will <I>not</I> be obvious. Furthermore, compilers are in no position to help us detect such an oversight, because they have no idea what we're doing (see also <a href="../EC/EI39_FR.HTM#7269" TARGET="_top">Item E39</A>).<SCRIPT>create_link(21);</SCRIPT>
</P>
<P><A NAME="dingp22"></A><A NAME="34947"></A>
This kind of type-based programming has a long history in C, and one of the things we know about it is that it yields programs that are essentially unmaintainable. Enhancement of such programs eventually becomes unthinkable. This is the primary reason why virtual functions were invented in the first place: to shift the burden of generating and maintaining type-based function calls from programmers to compilers. When we employ RTTI to implement double-dispatching, we are harking back to the bad old <NOBR>days.<SCRIPT>create_link(22);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp23"></A><A NAME="85623"></A>
The techniques of the bad old days led to errors in C, and they'll lead to errors in C++, too. In recognition of our human frailty, we've included a final <CODE>else</CODE> clause in the <CODE>collide</CODE> function, a clause where control winds up if we hit an object we don't know about. Such a situation is, in principle, impossible, but where were our principles when we decided to use RTTI? There are various ways to handle such unanticipated interactions, but none is very satisfying. In this case, we've chosen to throw an exception, but it's not clear how our callers can hope to handle the error any better than we can, since we've just run into something we didn't know <NOBR>existed.<SCRIPT>create_link(23);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp24"></A><font ID="mhtitle">Using Virtual Functions Only</font><SCRIPT>create_link(24);</SCRIPT>
</P>
<A NAME="34949"></A><P><A NAME="dingp25"></A>
There is a way to minimize the risks inherent in an RTTI approach to implementing double-dispatching, but before we look at that, it's convenient to see how to attack the problem using nothing but virtual functions. That strategy begins with the same basic structure as the RTTI approach. The <CODE>collide</CODE> function is declared virtual in <CODE>GameObject</CODE> and is redefined in each derived class. In addition, <CODE>collide</CODE> is overloaded in each class, one overloading for each derived class in the <NOBR>hierarchy:<SCRIPT>create_link(25);</SCRIPT>
</NOBR></P>
<A NAME="34950"></A>
<UL><PRE>class SpaceShip; // forward declarations
class SpaceStation;
class Asteroid;
<A NAME="53121"></A>
<A NAME="p233"></A>class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
virtual void collide(SpaceShip& otherObject) = 0;
virtual void collide(SpaceStation& otherObject) = 0;
virtual void collide(Asteroid& otherobject) = 0;
...
<A NAME="76336"></A>
};
<A NAME="34951"></A>
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherobject);
...
<A NAME="76337"></A>
};
</PRE>
</UL>
<P><A NAME="dingp26"></A><A NAME="34953"></A>
The basic idea is to implement double-dispatching as two single dispatches, i.e., as two separate virtual function calls: the first determines the <A HREF="./MIINTRFR.HTM#72671" TARGET="_top">dynamic type</a> of the first object, the second determines that of the second object. As before, the first virtual call is to the <CODE>collide</CODE> function taking a <CODE>GameObject&</CODE> parameter. That function's implementation now becomes startlingly <NOBR>simple:<SCRIPT>create_link(26);</SCRIPT>
</NOBR></P><A NAME="34954"></A>
<UL><PRE>void SpaceShip::collide(GameObject& otherObject)
{
otherObject.collide(*this);
}
</PRE>
</UL>
<P><A NAME="dingp27"></A><A NAME="34955"></A>
At first glance, this appears to be nothing more than a recursive call to <CODE>collide</CODE> with the order of the parameters reversed, i.e., with <CODE>otherObject</CODE> becoming the object calling the member function and <CODE>*this</CODE> becoming the function's parameter. Glance again, however, because this is <I>not</I> a recursive call. As you know, compilers figure out which of a set of functions to call on the basis of the <A HREF="./MIINTRFR.HTM#72671" TARGET="_top">static type</a>s of the arguments passed to the function. In this case, four different <CODE>collide</CODE> functions could be called, but the one chosen is based on the static type of <CODE>*this</CODE>. What is that static type? Being inside a member function of the class <CODE>SpaceShip</CODE>, <CODE>*this</CODE> must be of type <CODE>SpaceShip</CODE>. The call is therefore to the <CODE>collide</CODE> function taking a <CODE>SpaceShip&</CODE>, not the <CODE>collide</CODE> function taking a <CODE>GameObject&</CODE>.<SCRIPT>create_link(27);</SCRIPT>
</P>
<P><A NAME="dingp28"></A><A NAME="34956"></A>
All the <CODE>collide</CODE> functions are virtual, so the call inside <CODE>SpaceShip</CODE>::<CODE>collide</CODE> resolves to the implementation of <CODE>collide</CODE> corresponding to the real type of <CODE>otherObject</CODE>. Inside <I>that</I> implementation of <CODE>collide</CODE>, the real types of both objects are known, because the left-hand object is <CODE>*this</CODE> (and therefore has as its type the class imple<A NAME="p234"></A>menting the member function) and the right-hand object's real type is <CODE>SpaceShip</CODE>, the same as the declared type of the <NOBR>parameter.<SCRIPT>create_link(28);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp29"></A><A NAME="34957"></A>
All this may be clearer when you see the implementations of the other <CODE>collide</CODE> functions in <CODE>SpaceShip</CODE>:<SCRIPT>create_link(29);</SCRIPT>
</P>
<A NAME="34958"></A>
<UL><PRE>void SpaceShip::collide(SpaceShip& otherObject)
{
<I>process a SpaceShip-SpaceShip collision;</I>
}
<A NAME="34959"></A>
void SpaceShip::collide(SpaceStation& otherObject)
{
<I>process a SpaceShip-SpaceStation collision;</I>
}
<A NAME="34960"></A>
void SpaceShip::collide(Asteroid& otherObject)
{
<I>process a SpaceShip-Asteroid collision;</I>
}
</PRE>
</UL>
<P><A NAME="dingp30"></A><A NAME="34961"></A>
As you can see, there's no muss, no fuss, no RTTI, no need to throw exceptions for unexpected object types. There can be no unexpected object types — that's the whole point of using virtual functions. In fact, were it not for its fatal flaw, this would be the perfect solution to the double-dispatching <NOBR>problem.<SCRIPT>create_link(30);</SCRIPT>
</NOBR></P> <P><A NAME="dingp31"></A><A NAME="34962"></A>
The flaw is one it shares with the RTTI approach we saw earlier: each class must know about its siblings. As new classes are added, the code must be updated. However, the <I>way</I> in which the code must be updated is different in this case. True, there are no <CODE>if</CODE>-<CODE>then</CODE>-<CODE>else</CODE>s to modify, but there is something that is often worse: each class definition must be amended to include a new virtual function. If, for example, you decide to add a new class <CODE>Satellite</CODE> (inheriting from <CODE>GameObject</CODE>) to your game, you'd have to add a new <CODE>collide</CODE> function to each of the existing classes in the <NOBR>program.<SCRIPT>create_link(31);</SCRIPT>
</NOBR></P> <P><A NAME="dingp32"></A><A NAME="34963"></A>
Modifying existing classes is something you are frequently in no position to do. If, instead of writing the entire video game yourself, you started with an off-the-shelf class library comprising a video game application framework, you might not have write access to the <CODE>GameObject</CODE> class or the framework classes derived from it. In that case, adding new member functions, virtual or otherwise, is not an option. Alternatively, you may have <I>physical</I> access to the classes requiring modification, but you may not have <I>practical</I> access. For example, suppose you <I>were</I> hired by Nintendo and were put to work on programs using a library containing <CODE>GameObject</CODE> and other useful classes. Surely you wouldn't be the only one using that library, and Nintendo would probably be less than thrilled about recompiling every application using that library each time you decided to add a new type of ob<A NAME="p235"></A>ject to your program. In practice, libraries in wide use are modified only rarely, because the cost of recompiling everything using those libraries is too great. (See <a href="../EC/EI34_FR.HTM#6793" TARGET="_top">Item E34</A> for information on how to design libraries that minimize compilation <NOBR>dependencies.)<SCRIPT>create_link(32);</SCRIPT>
</NOBR></P> <P><A NAME="dingp33"></A><A NAME="34964"></A>
The long and short of it is if you need to implement double-dispatching in your program, your best recourse is to modify your design to eliminate the need. Failing that, the virtual function approach is safer than the RTTI strategy, but it constrains the extensibility of your system to match that of your ability to edit header files. The RTTI approach, on the other hand, makes no recompilation demands, but, if implemented as shown above, it generally leads to software that is unmaintainable. You pays your money and you takes your <NOBR>chances.<SCRIPT>create_link(33);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp34"></A><font ID="mhtitle">Emulating Virtual Function Tables</font><SCRIPT>create_link(34);</SCRIPT>
</P>
<P><A NAME="dingp35"></A><A NAME="34965"></A>
There is a way to improve those chances. You may recall from <a href="./MI24_FR.HTM#41284" TARGET="_top">Item 24</A> that compilers typically implement virtual functions by creating an array of function pointers (the vtbl) and then indexing into that array when a virtual function is called. Using a vtbl eliminates the need for compilers to perform chains of <CODE>if</CODE>-<CODE>then</CODE>-<CODE>else</CODE>-like computations, and it allows compilers to generate the same code at all virtual function call sites: determine the correct vtbl index, then call the function pointed to at that position in the <NOBR>vtbl.<SCRIPT>create_link(35);</SCRIPT>
</NOBR></P> <P><A NAME="dingp36"></A><A NAME="34969"></A>
There is no reason you can't do this yourself. If you do, you not only make your RTTI-based code more efficient (indexing into an array and following a function pointer is almost always more efficient than running through a series of <CODE>if</CODE>-<CODE>then</CODE>-<CODE>else</CODE> tests, and it generates less code, too), you also isolate the use of RTTI to a single location: the place where your array of function pointers is initialized. I should mention that the meek may inherit the earth, but the meek of heart may wish to take a few deep breaths before reading what <NOBR>follows.<SCRIPT>create_link(36);</SCRIPT>
</NOBR></P> <P><A NAME="dingp37"></A><A NAME="34971"></A>
We begin by making some modifications to the functions in the <CODE>GameObject</CODE> <NOBR>hierarchy:<SCRIPT>create_link(37);</SCRIPT>
</NOBR></P><A NAME="34972"></A>
<UL><PRE>class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
<A NAME="34973"></A>
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(SpaceShip& otherObject);
virtual void hitSpaceStation(SpaceStation& otherObject);
virtual void hitAsteroid(Asteroid& otherobject);
...
};
<A NAME="34974"></A>
<A NAME="p236"></A>void SpaceShip::hitSpaceShip(SpaceShip& otherObject)
{
<I>process a SpaceShip-SpaceShip collision;</I>
}
<A NAME="34975"></A>
void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
{
<I>process a SpaceShip-SpaceStation collision;</I>
}
<A NAME="34976"></A>
void SpaceShip::hitAsteroid(Asteroid& otherObject)
{
<I>process a SpaceShip-Asteroid collision;</I>
}
</PRE>
</UL><P><A NAME="dingp38"></A><A NAME="34977"></A>
Like the RTTI-based hierarchy we started out with, the <CODE>GameObject</CODE> class contains only one function for processing collisions, the one that performs the first of the two necessary dispatches. Like the virtual-function-based hierarchy we saw later, each kind of interaction is encapsulated in a separate function, though in this case the functions have different names instead of sharing the name <CODE>collide</CODE>. There is a reason for this abandonment of overloading, and we shall see it soon. For the time being, note that the design above contains everything we need except an implementation for <CODE>SpaceShip</CODE>::<CODE>collide</CODE>; that's where the various <CODE>hit</CODE> functions will be invoked. As before, once we successfully implement the <CODE>SpaceShip</CODE> class, the <CODE>SpaceStation</CODE> and <CODE>Asteroid</CODE> classes will follow <NOBR>suit.<SCRIPT>create_link(38);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp39"></A><A NAME="34978"></A>
Inside <CODE>SpaceShip</CODE>::<CODE>collide</CODE>, we need a way to map the <A HREF="./MIINTRFR.HTM#72671" TARGET="_top">dynamic type</a> of the parameter <CODE>otherObject</CODE> to a member function pointer that points to the appropriate collision-handling function. An easy way to do this is to create an associative array that, given a class name, yields the appropriate member function pointer. It's possible to implement <CODE>collide</CODE> using such an associative array directly, but it's a bit easier to understand what's going on if we add an intervening function, <CODE>lookup</CODE>, that takes a <CODE>GameObject</CODE> and returns the appropriate member function pointer. That is, you pass <CODE>lookup</CODE> a <CODE>GameObject</CODE>, and it returns a pointer to the member function to call when you collide with something of that <CODE>GameObject</CODE>'s <NOBR>type.<SCRIPT>create_link(39);</SCRIPT>
</NOBR></P>
<P><A NAME="dingp40"></A><A NAME="35739"></A>
Here's the declaration of <CODE>lookup</CODE>:<SCRIPT>create_link(40);</SCRIPT>
</P>
<A NAME="35730"></A>
<UL><PRE>class SpaceShip: public GameObject {
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
</PRE>
</UL><A NAME="35731"></A>
<UL><PRE> static HitFunctionPtr lookup(const GameObject& whatWeHit);
</PRE>
</UL><A NAME="35732"></A>
<UL><PRE> ...
};
</PRE>
</UL>
<P><A NAME="dingp41"></A><A NAME="35175"></A>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -