📄 mi31.htm
字号:
(*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceShip); (*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation); (*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid); return phm;}This will compile, but it's a bad idea. It entails doing something you should never do: lying to your compilers. Telling them that hitSpaceShip, hitSpaceStation, and hitAsteroid are functions expecting a GameObject argument is simply not true. hitSpaceShip expects a SpaceShip, hitSpaceStation expects a SpaceStation, and hitAsteroid expects an Asteroid. The casts say otherwise. The casts lie.More than morality is on the line here. Compilers don't like to be lied to, and they often find a way to exact revenge when they discover they've been deceived. In this case, they're likely to get back at you by generating bad code for functions you call through *phm in cases where GameObject's derived classes employ multiple inheritance or have virtual base classes. In other words, if SpaceStation, SpaceShip, or Asteroid had other base classes (in addition to GameObject), you'd probably find that your calls to collision-processing functions in collide would behave quite rudely.Consider again the A-B-C-D inheritance hierarchy and the possible object layout for a D object that is described in Item 24:Each of the four class parts in a D object has a different address. This is important, because even though pointers and references behave differently (see Item 1), compilers typically implement references by using pointers in the generated code. Thus, pass-by-reference is typically implemented by passing a pointer to an object. When an object with multiple base classes (such as a D object) is passed by reference, it is crucial that compilers pass the correct address the one corresponding to the declared type of the parameter in the function being called.But what if you've lied to your compilers and told them your function expects a GameObject when it really expects a SpaceShip or a SpaceStation? Then they'll pass the wrong address when you call the function, and the resulting runtime carnage will probably be gruesome. It will also be very difficult to determine the cause of the problem. There are good reasons why casting is discouraged. This is one of them.Okay, so casting is out. Fine. But the type mismatch between the function pointers a HitMap is willing to contain and the pointers to the hitSpaceShip, hitSpaceStation, and hitAsteroid functions remains. There is only one way to resolve the conflict: change the types of the functions so they all take GameObject arguments: class GameObject { // this is unchangedpublic: virtual void collide(GameObject& otherObject) = 0; ...};class SpaceShip: public GameObject {public: virtual void collide(GameObject& otherObject); // these functions now all take a GameObject parameter virtual void hitSpaceShip(GameObject& spaceShip); virtual void hitSpaceStation(GameObject& spaceStation); virtual void hitAsteroid(GameObject& asteroid); ...};Our solution to the double-dispatching problem that was based on virtual functions overloaded the function name collide. Now we are in a position to understand why we didn't follow suit here why we decided to use an associative array of member function pointers instead. All the hit functions take the same parameter type, so we must give them different names.Now we can write initializeCollisionMap the way we always wanted to: SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){ HitMap *phm = new HitMap; (*phm)["SpaceShip"] = &hitSpaceShip; (*phm)["SpaceStation"] = &hitSpaceStation; (*phm)["Asteroid"] = &hitAsteroid; return phm;}Regrettably, our hit functions now get a general GameObject parameter instead of the derived class parameters they expect. To bring reality into accord with expectation, we must resort to a dynamic_cast (see Item 2) at the top of each function: void SpaceShip::hitSpaceShip(GameObject& spaceShip){ SpaceShip& otherShip= dynamic_cast<SpaceShip&>(spaceShip); process a SpaceShip-SpaceShip collision;}void SpaceShip::hitSpaceStation(GameObject& spaceStation){ SpaceStation& station= dynamic_cast<SpaceStation&>(spaceStation); process a SpaceShip-SpaceStation collision;}void SpaceShip::hitAsteroid(GameObject& asteroid){ Asteroid& theAsteroid = dynamic_cast<Asteroid&>(asteroid); process a SpaceShip-Asteroid collision;}Each of the dynamic_casts will throw a bad_cast exception if the cast fails. They should never fail, of course, because the hit functions should never be called with incorrect parameter types. Still, we're better off safe than sorry.Using Non-Member Collision-Processing FunctionsWe now know how to build a vtbl-like associative array that lets us implement the second half of a double-dispatch, and we know how to encapsulate the details of the associative array inside a lookup function. Because this array contains pointers to member functions, however, we still have to modify class definitions if a new type of GameObject is added to the game, and that means everybody has to recompile, even people who don't care about the new type of object. For example, if Satellite were added to our game, we'd have to augment the SpaceShip class with a declaration of a function to handle collisions between satellites and spaceships. All SpaceShip clients would then have to recompile, even if they couldn't care less about the existence of satellites. This is the problem that led us to reject the implementation of double-dispatching based purely on virtual functions, and that solution was a lot less work than the one we've just seen.The recompilation problem would go away if our associative array contained pointers to non-member functions. Furthermore, switching to non-member collision-processing functions would let us address a design question we have so far ignored, namely, in which class should collisions between objects of different types be handled? With the implementation we just developed, if object 1 and object 2 collide and object 1 happens to be the left-hand argument to processCollision, the collision will be handled inside the class for object 1. If object 2 happens to be the left-hand argument to processCollision, however, the collision will be handled inside the class for object 2. Does this make sense? Wouldn't it be better to design things so that collisions between objects of types A and B are handled by neither A nor B but instead in some neutral location outside both classes?If we move the collision-processing functions out of our classes, we can give clients header files that contain class definitions without any hit or collide functions. We can then structure our implementation file for processCollision as follows: #include "SpaceShip.h"#include "SpaceStation.h"#include "Asteroid.h"namespace { // unnamed namespace see below // primary collision-processing functions void shipAsteroid(GameObject& spaceShip, GameObject& asteroid); void shipStation(GameObject& spaceShip, GameObject& spaceStation); void asteroidStation(GameObject& asteroid, GameObject& spaceStation); ... // secondary collision-processing functions that just // implement symmetry: swap the parameters and call a // primary function void asteroidShip(GameObject& asteroid, GameObject& spaceShip) { shipAsteroid(spaceShip, asteroid); } void stationShip(GameObject& spaceStation, GameObject& spaceShip) { shipStation(spaceShip, spaceStation); } void stationAsteroid(GameObject& spaceStation, GameObject& asteroid) { asteroidStation(asteroid, spaceStation); } ... // see below for a description of these types/functions typedef void (*HitFunctionPtr)(GameObject&, GameObject&); typedef map< pair<string,string>, HitFunctionPtr > HitMap; pair<string,string> makeStringPair(const char *s1, const char *s2); HitMap * initializeCollisionMap(); HitFunctionPtr lookup(const string& class1, const string& class2);} // end namespacevoid processCollision(GameObject& object1, GameObject& object2){ HitFunctionPtr phf = lookup(typeid(object1).name(), typeid(object2).name()); if (phf) phf(object1, object2); else throw UnknownCollision(object1, object2);}Note the use of the unnamed namespace to contain the functions used to implement processCollision. Everything in such an unnamed namespace is private to the current translation unit (essentially the current file) it's just like the functions were declared static at file scope. With the advent of namespaces, however, statics at file scope have been deprecated, so you should accustom yourself to using unnamed namespaces as soon as your compilers support them.Conceptually, this implementation is the same as the one that used member functions, but there are some minor differences. First, HitFunctionPtr is now a typedef for a pointer to a non-member function. Second, the exception class CollisionWithUnknownObject has been renamed UnknownCollision and modified to take two objects instead of one. Finally, lookup must now take two type names and perform both parts of the double-dispatch. This means our collision map must now hold three pieces of information: two types names and a HitFunctionPtr.As fate would have it, the standard map class is defined to hold only two pieces of information. We can finesse that problem by using the standard pair template, which lets us bundle the two type names together as a single object. initializeCollisionMap, along with its makeStringPair helper function, then looks like this: // we use this function to create pair<string,string>// objects from two char* literals. It's used in// initializeCollisionMap below. Note how this function// enables the return value optimization (see Item 20).namespace { // unnamed namespace again see below pair<string,string> makeStringPair(const char *s1, const char *s2) { return pair<string,string>(s1, s2); }} // end namespacenamespace { // still the unnamed namespace see below HitMap * initializeCollisionMap() { HitMap *phm = new HitMap;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -