📄 mi31.htm
字号:
...};void SpaceShip::hitSpaceShip(SpaceShip& otherObject){ process a SpaceShip-SpaceShip collision;}void SpaceShip::hitSpaceStation(SpaceStation& otherObject){ process a SpaceShip-SpaceStation collision;}void SpaceShip::hitAsteroid(Asteroid& otherObject){ process a SpaceShip-Asteroid collision;}Like the RTTI-based hierarchy we started out with, the GameObject 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 collide. 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 SpaceShip::collide; that's where the various hit functions will be invoked. As before, once we successfully implement the SpaceShip class, the SpaceStation and Asteroid classes will follow suit.Inside SpaceShip::collide, we need a way to map the dynamic type of the parameter otherObject 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 collide using such an associative array directly, but it's a bit easier to understand what's going on if we add an intervening function, lookup, that takes a GameObject and returns the appropriate member function pointer. That is, you pass lookup a GameObject, and it returns a pointer to the member function to call when you collide with something of that GameObject's type.Here's the declaration of lookup: class SpaceShip: public GameObject {private: typedef void (SpaceShip::*HitFunctionPtr)(GameObject&); static HitFunctionPtr lookup(const GameObject& whatWeHit); ...};The syntax of function pointers is never very pretty, and for member function pointers it's worse than usual, so we've typedefed HitFunctionPtr to be shorthand for a pointer to a member function of SpaceShip that takes a GameObject& and returns nothing.Once we've got lookup, implementation of collide becomes the proverbial piece of cake: void SpaceShip::collide(GameObject& otherObject){ HitFunctionPtr hfp = lookup(otherObject); // find the function to call if (hfp) { // if a function was found (this->*hfp)(otherObject); // call it } else { throw CollisionWithUnknownObject(otherObject); }}Provided we've kept the contents of our associative array in sync with the class hierarchy under GameObject, lookup must always find a valid function pointer for the object we pass it. People are people, however, and mistakes have been known to creep into even the most carefully crafted software systems. That's why we still check to make sure a valid pointer was returned from lookup, and that's why we still throw an exception if the impossible occurs and the lookup fails.All that remains now is the implementation of lookup. Given an associative array that maps from object types to member function pointers, the lookup itself is easy, but creating, initializing, and destroying the associative array is an interesting problem of its own.Such an array should be created and initialized before it's used, and it should be destroyed when it's no longer needed. We could use new and delete to create and destroy the array manually, but that would be error-prone: how could we guarantee the array wasn't used before we got around to initializing it? A better solution is to have compilers automate the process, and we can do that by making the associative array static in lookup. That way it will be created and initialized the first time lookup is called, and it will be automatically destroyed sometime after main is exited (see Item E47).Furthermore, we can use the map template from the Standard Template Library (see Item 35) as the associative array, because that's what a map is: class SpaceShip: public GameObject {private: typedef void (SpaceShip::*HitFunctionPtr)(GameObject&); typedef map<string, HitFunctionPtr> HitMap; ...};SpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit){ static HitMap collisionMap; ...}Here, collisionMap is our associative array. It maps the name of a class (as a string object) to a SpaceShip member function pointer. Because map<string, HitFunctionPtr> is quite a mouthful, we use a typedef to make it easier to swallow. (For fun, try writing the declaration of collisionMap without using the HitMap and HitFunctionPtr typedefs. Most people will want to do this only once.)Given collisionMap, the implementation of lookup is rather anticlimactic. That's because searching for something is an operation directly supported by the map class, and the one member function we can always (portably) call on the result of a typeid invocation is name (which, predictably11, yields the name of the object's dynamic type). To implement lookup, then, we just find the entry in collisionMap corresponding to the dynamic type of lookup's argument.The code for lookup is straightforward, but if you're not familiar with the Standard Template Library (again, see Item 35), it may not seem that way. Don't worry. The comments in the function explain what's going on. SpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit){ static HitMap collisionMap; // we'll see how to // initialize this below // look up the collision-processing function for the type // of whatWeHit. The value returned is a pointer-like // object called an "iterator" (see Item 35). HitMap::iterator mapEntry= collisionMap.find(typeid(whatWeHit).name()); // mapEntry == collisionMap.end() if the lookup failed; // this is standard map behavior. Again, see Item 35. if (mapEntry == collisionMap.end()) return 0; // If we get here, the search succeeded. mapEntry // points to a complete map entry, which is a // (string, HitFunctionPtr) pair. We want only the // second part of the pair, so that's what we return. return (*mapEntry).second;}The final statement in the function returns (*mapEntry).second instead of the more conventional mapEntry->second in order to satisfy the vagaries of the STL. For details, see page 96.Initializing Emulated Virtual Function TablesWhich brings us to the initialization of collisionMap. We'd like to say something like this, // An incorrect implementationSpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit){ static HitMap collisionMap; collisionMap["SpaceShip"] = &hitSpaceShip; collisionMap["SpaceStation"] = &hitSpaceStation; collisionMap["Asteroid"] = &hitAsteroid; ...}but this inserts the member function pointers into collisionMap each time lookup is called, and that's needlessly inefficient. In addition, this won't compile, but that's a secondary problem we'll address shortly.What we need now is a way to put the member function pointers into collisionMap only once when collisionMap is created. That's easy enough to accomplish; we just write a private static member function called initializeCollisionMap to create and initialize our map, then we initialize collisionMap with initializeCollisionMap's return value: class SpaceShip: public GameObject {private: static HitMap initializeCollisionMap(); ...};SpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit){ static HitMap collisionMap = initializeCollisionMap(); ...}But this means we may have to pay the cost of copying the map object returned from initializeCollisionMap into collisionMap (see Items 19 and 20). We'd prefer not to do that. We wouldn't have to pay if initializeCollisionMap returned a pointer, but then we'd have to worry about making sure the map object the pointer pointed to was destroyed at an appropriate time.Fortunately, there's a way for us to have it all. We can turn collisionMap into a smart pointer (see Item 28) that automatically deletes what it points to when the pointer itself is destroyed. In fact, the standard C++ library contains a template, auto_ptr, for just such a smart pointer (see Item 9). By making collisionMap a static auto_ptr in lookup, we can have initializeCollisionMap return a pointer to an initialized map object, yet never have to worry about a resource leak; the map to which collisionMap points will be automatically destroyed when collisionMap is. Thus: class SpaceShip: public GameObject {private: static HitMap * initializeCollisionMap(); ...};SpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit){ static auto_ptr<HitMap> collisionMap(initializeCollisionMap()); ...}The clearest way to implement initializeCollisionMap would seem to be this, SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){ HitMap *phm = new HitMap; (*phm)["SpaceShip"] = &hitSpaceShip; (*phm)["SpaceStation"] = &hitSpaceStation; (*phm)["Asteroid"] = &hitAsteroid; return phm;}but as I noted earlier, this won't compile. That's because a HitMap is declared to hold pointers to member functions that all take the same type of argument, namely GameObject. But hitSpaceShip takes a SpaceShip, hitSpaceStation takes a SpaceStation, and, hitAsteroid takes an Asteroid. Even though SpaceShip, SpaceStation, and Asteroid can all be implicitly converted to GameObject, there is no such conversion for pointers to functions taking these argument types.To placate your compilers, you might be tempted to employ reinterpret_casts (see Item 2), which are generally the casts of choice when converting between function pointer types: // A bad idea...SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){ HitMap *phm = new HitMap;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -