1.3 Item M31: Let function determine how virtual according to more than one object
Sometimes, borrow Jacqueline Susann: one is not enough. For example, you have a brilliant, lofty, rich and salary programmer work, a famous software company in Redmond, WSHINGTON - of course, I am talking about Nintendo. In order to obtain the manager, you may decide to write a video game. The background of the game is happening in space, with spacecraft, space station and small planet.
The spacecraft, space station and small planet in your constructed world, they may collide with each other. Suppose its rules are:
* If the spacecraft and space stations are in low speed, the spacecraft will park into the space station. Otherwise, they will be damaged by the relative speed.
* If the spacecraft is colliding with the spacecraft, or the space station collides with the space station, participants are in proportion to the relative speed damage.
* If the asteroid collides with the spacecraft or space station, the asteroid is destroyed. If the asteroid is large, the spacecraft or space station is also destroyed.
* If two asteroids collide, fragmented into smaller small planets and sputter in each direction.
This seems to be a boring game, but it is already enough for our example, considering how to organize C code to handle collisions between objects.
We start with the common characteristics of the assignment of spacecraft, space station and asteroid. At least, they are exercise, so there is a speed to describe this movement. Based on this, naturally design a base class, and they can inherit. In fact, such a class is almost always abstract base class, and if you pay attention to the warning in Item M33, the base class is always abstract. Therefore, the inheritance system is like this:
GameObject
| | | | |
/ | /
/ | /
/ | /
/ | /
Spaceship SpaceStation Asteroid
Class GameObject {...};
Class Spaceeship: Public GameObject {...};
Class SpaceStation: Public GameObject {...};
Class Asteroid: Public GameObject {...};
Now, suppose you start entering the program inside, write code to detect and process collisions between objects. You will ask such a function:
Void Checkforcollision (GameObject & Object1,
GameObject & object2)
{
IF (theyjust1, object2)) {
ProcessCollision (Object1, Object2);
}
Else {
...
}
}
The problem is coming. When you call processCollision (), you know that Object1 and Object2 have collided, and you know the results that happen will depend on the true type of Object1 and Object2, but you don't know its true type; you know that only they are GameObject object. If the process of collision depends only on the dynamic type of Object1, you can set processCollision () to virtual functions and call Object1.ProcessColliiION (Object2). If only the dynamic type of Object2, it can also be processed. But now, depending on the dynamic type of the two objects. The virtual function system can only act on an object, it is not enough to solve the problem.
What you need is a virtual function that acts on multiple objects. C does not provide such a function. However, you have to implement the above requirements. What should I do now? One way is to throw away C , transvest into other languages. For example, you can use the Clos (Common Lisp Object System). CLOS supports most of the object-oriented function calling system that can only be imagined: Multi-method. Multi-Method is a virtual function on any number of parameters, and CLOS further provides the characteristics that explicitly control "how to be overloaded multi-method will" call ".
Let us assume that you must implement C , so you must find a method to solve this problem with "Double Dispatch". (This name comes from Object-Oriented Programming Comminity, where the term "Message Dispatch" is "Message Dispatch", and the virtual call for the two parameters is implemented through "Double Dispatch", which is wide, in multiple parameters The virtual function is called "Multiple Dispatch".) There are several ways to consider. But there is no shortcomings, this should not be strange. C does not provide "Double Dispatch" directly, so you have to complete your compiler when you implement the virtual function (see Item M24). If it is easy, we may do what you have, and program it with C language. We didn't, and we can't, so we tight your seat belt, there is a rogue.
* Add RTTI with virtual functions
The virtual function implements a single scheduling, which is just half of us; the compiler implements the virtual function for us, so we declare a virtual function Collide in GameObject. This function is derived in a usual form:
Class gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject) = 0;
...
}
Class Spaceeship: public gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject);
...
}
I only wrote the situation of derived class spaceship, the form of SpaceStation and Asteroid is exactly the same.
The most common method for achieving dual scheduling is the IF ... The ELSE chain of the virtual functional architecture. In this glare system, we first discovered the true type of OtherObject and then tested all possible:
// if We Collide with an Object of Unknown Type, WE
// throw an exception of this type:
Class CollisionwithunknownObject {
PUBLIC:
CollisionwithunknownObject (GameObject & whatWehit);
...
}
Void Spaceeship :: Collide (GameObject & OtherObject)
{
Const type_info & objectType = typeid (OtherObject);
IF (ObjectType == TypeId (spaceship)) {
Spaceeship & ss = static_cast
}
Else IF (ObjectType == TypeId (spacStation) {
SpaceStation & SS =
Static_cast
PROCESS A SpaceShip-SpaceStation COLLISION;
}
Else if (ObjectType == TypeId (Asteroid)) {
Asteroid & a = static_cast
Process a spaceship-asseteroid collision;
}
Else {
Throw CollisionwithunknownObject (OtherObject);
}
}
Note that we need to detect just an object type. The other is * this, its type is judged by virtual function system. We are now in the Member function of SpaceShip, so * this is definitely a spaceship object, so we only need to find the type of OtherObject.
The code here is not complicated. It is easy to implement. It is also easy to make it work. RTTI is only a little disturbing: it just looks harmless. The actual danger comes from the last ELSE statement, and throws an exception here.
Our price is almost almost abandoned because each Collide function must know the version in other compatriots. In particular, if we add a new class, we must update each RTTI-based if ... Then ... ELSE chain to handle this new type. Even if I just forgotten, the program will have a bug, and it is still uncomfortable. The compiler also can't help us check this negligence because they don't know what we are doing (see Item E39).
This type of related program has been very historical in C language, and we also know that such programs are inherently no maintenanceability. Extension This program is ultimately unimaginable. This is an idea reason for introducing virtual functions: the burden called by the function called the generated and maintenance type is transferred to the compiler. When we use RTTI to implement dual scheduling, we are returning to the past suffering.
This excessive skill causes an error in C language, and their C language still causes errors. Recognizing our own fragile, we add the last ELSE statement in the Collide function to handle if an unknown type is encountered. This situation is not possible in principle, but how do we know when we decided to use RTTI? There are many ways to deal with this unpredictable interaction, but there is no very satisfaction. In this example, we chose to throw an exception, but unable to imagine that the caller is better than us, because we have encountered something we don't know about it.
* Only virtual functions
In fact, there is a way to achieve the low-scheduling inherent risks with RTTI to minimize, but let us take a look at how only the virtual function can solve the dual scheduling problem. This method and the RTTI method have the same basic architecture. The Collide function is declared as a virtual and is defined by all derived class, in addition, it is also overloaded by each class, each overload processing a derived type:
Class spaceship; // forward declarations
Class spachen;
Class asteroid;
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;
...
}
Class Spaceeship: public gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject);
Virtual Void Collide (SpaceShip & OtherObject);
Virtual Void Collide (SpaceStation & OtherObject);
Virtual Void Collide (Asteroid & OtherObject);
...
}
The basic principle is to implement double scheduling with two single scheduling, that is, two separate virtual functions calls: the first time determines the dynamic type of the first object, and determine the second object dynamic type. As in front, the first virtual function call is the parameter of the GameObject type. It is a surprisingly simple.
Void Spaceeship :: Collide (GameObject & OtherObject)
{
OtherObject.collide (* this);
}
Looking at the roughness, it is looped in the order of parameters, that is, the starting OtherObject becomes an object that calls member functions, and * this is a parameter. But take a closer look, it is not a loop call. You know, the compiler determines which one in the set of functions according to the static type of the parameters. Here, there are four different Collide functions that can be called, but according to the static type of * this. What is the current static type? Because it is in the Member function in SpaceShip, * this is definitely a spaceship type. The call will be the collide function to accept the spaceship parameter instead of the collide function with the GameojBect type parameter.
All collide functions are virtual functions, so call the Collide version implemented in the otherbject real type in SpaceShip :: ColLide. In this release, the real type of the two objects knows, the left is * this (type of this function), the real type of the object on the right is SpaceShip (declare type type).
After reading the implementation of other Collide in the spaceship class, it is clearer:
Void Spaceeship :: Collide (SpaceShip & OtherObject)
{
Process a spaceship-spaceship collision;
}
Void Spaceeship :: Collide (SpaceStation & OtherObject)
{
PROCESS A SpaceShip-SpaceStation COLLISION;
}
Void SpaceShip :: Collide (Asteroid & OtherObject)
{
Process a spaceship-asseteroid collision;
}
You have seen it, it is not confusing, nor does it trouble, there is no RTTI, and it does not need to be throwing it for an unexpected object type. If you don't intentionally, this is the benefits of using virtual functions. In fact, if there is no deadly defect, it is the perfect solution for achieving dual scheduling issues. This defect is the same as the RTTI method as seen earlier: each class must know its compatriots. All code must be updated when adding a new class. However, the update method is different from the front. Indeed, there is no IF ... Then ... Else needs to be modified, but it is usually worse: each class needs to add a new virtual function. For this reason, if you decide to add a new class Satellite (inherited to GameObjcet), you must add a COLLIDE function for each existing class.
Modifying existing categories often can't do it. For example, you are not writing the entire game, just a support library under the program framework, you may have no right to modify the GameObject class or from its frequent framework class. At this point, it is impossible to add a new member function (virtual or unimpled). That is, you theory there is an operation that needs to be modified, but it doesn't actually. For an alteration, you are employed in Nitndo, programming with a run library containing GameObject and other needs. Of course, it is not only a person who is using this library. All companies will vibrate every time you decide to add a new type in your code, all programs need to be recompiled. In practice, the widely used library is extremely modified, because the cost of re-compiling all the programs that use this library is too big. (See Item M34 to learn how to design compilation dependence to the lowest run.)
Summary is: If you need to achieve dual scheduling, the best way is to modify the design to cancel this need. If you can't do it, the method of virtual functions is safe than RTTI, but it limits the controlability of your program (depending on whether you have the right to modify the header file). On the other hand, the method of RTTI does not need to be recompiled, but usually causes the code to not maintain. Do your choice!
* Simulated virtual function table
There is a way to increase the choice. You can review the Item M24, the compiler typically create a function key array (VTBL) to implement the virtual function and perform the subscript index in this array when the virtual function is called. Using VTBL, the compiler avoids the use of if ... the ... ELSE chain, and can generate the same code in all call virtual functions: determine the correct VTBL subscript, then call the pointer stored in the VTBL. Pointing function.
No reason, you can't do this. If this is done, not only the RTTI-based code is more efficient (down the index of the indexed function pointer is usually efficient than if ... then ... Else, the result is small), and the RTTI is also The range is limited to: your initialization of the functional pointer array. Remind, it is best to do a deep breath before looking at the following content (I shop Mention That the Meek May Wish to Take A Few Deep Breaths Before Reading What Follows).
For some modifications to the function in the GameObjcet inheritance system:
Class gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject) = 0;
...
}
Class Spaceeship: public gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject);
Virtual Void HitspaceShip (Spaceeship & OtherObject);
Virtual Void HitspaceStation (SpaceStation & OtherObject);
Virtual Void HitasterOid (Asteroid & OtherObject);
...
}
Void Spaceeship :: HitspaceShip (Spaceeship & OtherObject)
{
Process a spaceship-spaceship collision;
}
Void SpaceShip :: HitsPacStation (SpaceStation & OtherObject)
{
PROCESS A SpaceShip-SpaceStation COLLISION;
}
Void Spaceship :: Hitasteroid (Asteroid & OtherObject)
{
Process a spaceship-asseteroid collision;
}
And the RTTI-based method used at the beginning is similar, the GameObjcet class has only one function of the collision, which implements the first weight of the necessary dual scheduling. Similar to the later virtual function, each collision is handled by a separate function, but the difference is that these functions have different names, rather than being called Collide. Abandoning overload is caused, you will soon see it. Note that in the above design, all other things needed, in addition to unsuccessful SpaceShip :: Collide (this is where different collision functions is called). As before, it has achieved the spaceship class, the SpaceStation class, and the AsteroID class.
In SpaceShip :: Collide, we need a method to map the dynamic type of the parameter OtherObject to a member function pointer (pointing to an appropriate collision process). A simple way is to create a mapping table, a given class name, corresponding to the appropriate member function pointer. Use one such mapping table directly to implement Collide is feasible, but if an intermediate function lookup is added, it will be better understood. The Lookup function accepts a GameObject parameter to return the corresponding member function pointer.
This is the declaration of Lookup:
Class Spaceeship: public gameObject {
Private:
TypeDef void (spaceObject) (GameObject &);
Static HitFunctionPtr Lookup (Const GameObject & WhatweHit);
...
}
The syntax of the function pointer is not very beautiful, and the member function pointer is worse, so we have made a type of redefine.
Since there is lookup, the Collide is achieved as follows:
Void Spaceeship :: Collide (GameObject & OtherObject)
{
HitFunctionPtr HFP =
LOOKUP (OtherObject); // Find the function to call
IF (HFP) {// if a function tasn
(this -> * HFP) (OtherObject); // Call it}
Else {
Throw CollisionwithunknownObject (OtherObject);
}
}
If we can keep the mapping table and the inheritance of GameObject's inheritance hierarchy, lookup can always find a valid function pointer corresponding to the incoming object. If people are only a person, even if it is carefully, the error will also drill into the software. That is why we check the return value of the Lookup and throw it during its failure.
The rest is to implement Lookup. After providing an object type to a mapping table of a member function pointer, you can easily implement itself, but the mapping table for creation, initialization, and destructive is a interesting issue.
Such arrays should be constructed and initialized before it is used, and they are not needed to be tone. We can use new and delete to create and destructure it, but how do you guarantee that it is not used before initialization? Better solution is to let the compiler are complete, and this array is static in Lookup. Thus, it constructs and initialized before the first callup, which is automatically destructed at some time after main exit (see Item E47).
Moreover, we can use the MAP template provided by the standard template library to implement the mapping table because this is the function of MAP:
Class Spaceeship: public gameObject {
Private:
TypeDef void (spaceObject) (GameObject &);
Typedef Map
...
}
Spaceeship :: HitFunctionPtr
Spaceeship :: Lookup (const gameObject & whatwehit)
{
Static Hitmap CollisionMap;
...
}
Here, CollisionMap is our mapping table. It maps the class name (a String object) to a member function pointer of SpaceShip. Because Map
After giving CollisionMap, Lookup has achieved some tiger's tail. Because the search work is a direct support for the MAP class, and we can always call (portable) a member function that can be called on the return result of typeid () is Name () (indicate (Note 11), it returns the dynamics of the object The name of the type). So, realize the LOOKUP, just finding its corresponding item in CollisionMap based on the dynamic type of the ginseng.
Lookup's code is simple, but if you are not familiar with the standard template library (see Item M35 again), it will not be simple. Don't worry, the comments in the program explain what every step is.
Spaceeship :: HitFunctionPtr
Spaceeship :: Lookup (const gameObject & whatwehit)
{
Static Hitmap CollisionMap; // We'll See How To 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 ITERATOR ".
Hitmap :: item 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. Mapen
// 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 last sentence is Return (* mapentry). Second instead of the custom MapenTry-> Second to meet the strange behavior of STL. See Item M18 for specific reasons.
* Initialize analog virtual function table
Now come to see the initialization of CollisionMap. We really want to do this:
// an incorrect implementation
Spaceeship :: HitFunctionPtr
Spaceeship :: Lookup (const gameObject & whatwehit)
{
Static Hitmap CollisionMap;
CollisionMap ["spaceship"] = & HITSPACESHIP;
CollisionMap ["SpacStation"] = & HITSPACESTATION;
CollisionMap ["Asteroid"] = & HitasterOid;
...
}
However, this will add member function pointer to CollisionMap at each time called Lookup, which is unnecessary overhead. And it will not compile, but this is the second problem that will be discussed.
What we need is to add only the member function pointer to CollisionMap once in the CollisionMap constructed. This is easy to complete; we only need to write a private static member function initializeCollisionMap to construct and initialize our mapping table, then use it to initialize CollisionMap:
Class Spaceeship: public gameObject {
Private:
Static Hitmap InitializeCollisionMap ();
...
}
Spaceeship :: HitFunctionPtr
Spaceeship :: Lookup (const gameObject & whatwehit)
{
Static Hitmap CollisionMap = INITIALIZECOLLISIONMAP ();
...
}
But this means we have to pay the cost of copy assignment (see Item M19 and M20). We don't want to do this. If INITIALIZECOLLISIONMAP () returns a pointer, we don't need to pay this price, but it is necessary to worry that the MAP object pointing to the pointer can be destructed when it is appropriate.
Fortunately, there is a two-whole method. We can change CollisionMap to a smart pointer (see Item M28) which will be directed to the delete when you aretructed. In fact, the standard C run library provides template auto_ptr, which is such a smart pointer (see Item M9). By declaring the CollisionMap in the LOOKUP, we can let the initializecollisionmap returns a pointer to the initialized MAP object, no need to worry about the resource leakage; CollisionMap points to the MAP object to be analyzed when collisinmap Structure. So: Class Spaceship: Public GameObject {
Private:
Static Hitmap * InitializeCollisionMap ();
...
}
Spaceeship :: HitFunctionPtr
Spaceeship :: Lookup (const gameObject & whatwehit)
{
Static auto_ptr
CollisionMap (InitializationCollisionMap ());
...
}
The clearest way to achieve initializecollisionmap looks like this:
Spaceeship :: Hitmap * Spaceeship :: InitializeCollisionMap ()
{
Hitmap * phm = new hitmap;
(* phM) ["spaceship"] = & HITSPACESHIP;
(* phM) ["spacStation"] = & HITSPACESTATION;
(* phM) ["asteroid"] = & Hitasteroid;
Return PHM;
}
But as I pointed out, this can't be compiled. Because Hitmap is declared as pointers that point to member functions, they all take the same parameter type, which is GameObject. However, HitspaceShip is a spaceship parameter, and the HitsPacStation is SpacStation, and HitasterOid is Asteroid. Although SpaceShip, SpaceStation and AsteroID can be implicitly converted to GameObject, the function pointer to these parameter types may have no such conversion relationship.
In order to flatter your compiler, you may want to use ReinterPret_casts (see Item M2), and it is usually discarded in the type conversion of the function pointer:
// a bad idea ...
Spaceeship :: Hitmap * Spaceeship :: InitializeCollisionMap ()
{
Hitmap * phm = new hitmap;
(* phM) ["spaceship"] =
Reinterpret_cast
(* phM) ["spacStation"] =
Reinterpret_cast
(* phM) ["asteroid"] =
Reinterpret_cast
Return PHM;
}
This can be compiled, but a bad idea. It must be accompanied by some things you should never do: lying on your compiler. Tell the compiler, HitspaceShip, HitspaceStation, and Hitasteroid expect a GameObject type parameter, and the fact is not this. HitspaceShip expects a spaceship, HitsPacStation expects a spacStation, Hitasteroid expects an Asteroid. These CAST is about something, they lie. Not just violating the principle, there is still danger. The compiler doesn't like to be lying. When they find being deceived, they often find a way of retaliation. Here, they are likely to retest you by generating the wrong code, when you call the function, while the corresponding GameObject derived class is multiple inherited or have a virtual base class. If spacstation. Spaceeship or AsteroID has other base classes in addition to GameObject, you may find that when you call your collision processing function here, its behavior is very rude.
Take a look at the inheritance system of the A-B-D D in Item M24 and the memory layout of the object of D.
A b data members
/ / VPTR
/ / Pointer to Virtual Base CLSS
B C C Data MEMBERS
/ / VPTR
/ / Pointer TO Virtual Base Class
D D Data MEMBERS
A data members
VPTR
Some of the four classes in D, their address is different. This is important because although the behavior of pointers and references are different (see Item M1), the code generated by the compiler is usually implemented by a pointer. Thus, the pass reference is usually implemented by a pointer. When a number of objects with multiple base classes (such as D's object) passes the reference, the most important thing is that the compiler is to pass the correct address - the one of the type of parametric that is compatible with the modified function.
But if you lie to your compiler say your function expects a GameObject, what happens when you want a spaceship or a spacStation? The compiler will pass to your wrong address, causing an error error. And it will be very difficult to locate the cause. There are a lot of good reasons to explain why it is not recommended to use type conversion, which is one of them.
OK, do not use type conversion. But the function pointer type does not match, there is only one way: turn all the functions to accept the GameObject type:
Class GameObject {// this is unchanged
PUBLIC:
Virtual Void Collide (GameObject & OtherObject) = 0;
...
}
Class Spaceeship: public gameObject {
PUBLIC:
Virtual Void Collide (GameObject & OtherObject);
// There Functions Now All Take a GameObject Parameter
Virtual Void HitspaceShip (GameObject & SpaceShip);
Virtual Void HitspaceStation; Virtual Void HitasterOid (GameObject & asset);
...
}
In the method of solving the two-dimensional scheduling problem based on the virtual function, we overload the function called Collide. Now, we understand why there is no photo and use a group of member function pointers. All collision processing functions have the same parameter type, so it is necessary to give them different names.
Now, we can write the InitializationCollisionMap function in the way we have always expected:
Spaceeship :: Hitmap * Spaceeship :: InitializeCollisionMap ()
{
Hitmap * phm = new hitmap;
(* phM) ["spaceship"] = & HITSPACESHIP;
(* phM) ["spacStation"] = & HITSPACESTATION;
(* phM) ["asteroid"] = & Hitasteroid;
Return PHM;
}
Unfortunately, our collision function is now getting a more basic CameObject parameter rather than the derived type of derived class. To get what we expect, you must use Dynamic_CAST in every collision function (see Item M2):
Void Spaceeship :: HitspaceShip (GameObject & SpaceShip)
{
Spaceeship & Othership =
Dynamic_cast
Process a spaceship-spaceship collision;
}
Void SpaceShip :: HitsPacStation (GameObject & SpaceStation)
{
SpacStation & station =
Dynamic_cast
PROCESS A SpaceShip-SpaceStation COLLISION;
}
Void Spaceeship :: HitasterOid (GameObject & Asteroid)
{
AskOID & THEASTEROID =
Dynamic_cast
Process a spaceship-asseteroid collision;
}
If the conversion fails, Dynamic_cast will throw a Bad_cast exception. Of course, they never fail because the collision function is called when the collision function is called. Just, cautious some better.
* Use non-members' collision processing functions
We now know how to construct a map similar to VTBL maps to implement the second part of the two scheduling, and we also know how to encapsulate the realization details of the mapping table in the Lookup function. Because this table contains a pointer to the member function, it still needs to modify the definition of the class when adding a new GameObject type, which means that everyone must recompile, even if they don't care at this new type. For example, if you add a Satellite type, we have to add a function that handles the collision between spaceship and Satellite objects in the SpaceShip class. All spaceship users have to recompile, even if they don't care about the existence of the Satellite object. This problem will cause us to rejoice only use virtual functions to implement dual scheduling, and the solution is to do small modifications. If the pointer contained in the mapping table points to the non-member function, then there is no recoiling problem. Moreover, transfer to non-members' collision processing will let us find a problem that has been neglected, that is, which class should handle collisions between different types of objects? In the previous design, if the object 1 and the object 2 collide, the exact object 1 is the parameters of the left side of ProcessCollision, the collision will be processed in the class of the object 1; if the object 2 is the left parameters, the collision is in the object 2 Treatment in the class. Is this special meaning? This is not this better: the collisions between the objects of type A and type B should neither be treated in B in A, and some neutrality outside of the two?
If you move the collision process from the class, we don't have to bring any collision processing functions when we provide a class definition of the class definition. We can organize files that implement collision processing functions into this:
#include "spaceship.h"
#include "spacStation.h"
#include "askOID.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)
{ShipSTeroid (spaceship, asseteroid);
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 type / functionstypedEf void (* HitFunctionPtr) (GameObject &, GameObject &);
Typedef Map
Pair
Const char * s2);
Hitmap * InitializeCollisionMap ();
HitFunctionPtr Lookup (const string & class1,
Const string & class2;
} // end namespace
Void 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 that an unnamed namespace contains the functions required to implement the collision process function. The unnamed namespace is the current compilation unit (in fact, the current file) private - very icing to the Function of Static's STATIC. After having a namespace, Static's Static is not approved, you should use the unknown namespace as soon as possible (as long as the compiler is supported) as soon as possible.
In theory, this implementation and the version of the member function are the same, only a few slight differences. First, HitFunctionPTR is now a redefinition of a pointer type pointing to non-member functions. Second, the unexpected class CollisionwithunkNownObject is called UnknownCollision, third, its constructor requires two objects to make parameters and no longer one. This also means that our mapping requires three messages: two type names, a HitFunctionPtr.
The standard MAP class is defined as only two information. We can solve this problem by using standard PAIR templates, pair allows us to bundle two types into an object. With the help of MakeStringPair, the implementation of InitializationCollisionMap is as follows:
// We use this function to create pair
// Objects from two char * literals. It's buy in
// InitializecollisionMap Below. Note How this function
// Enables The Return Value Optimization (See Item 20).
Namespace {// unnamed namespace again - See Below
Pair
Const char * S2)
{Return Pair
} // end namespace
Namespace {// still the unnamed namespace - See Below
Hitmap * InitializeCollisionMap () {
Hitmap * phm = new hitmap;
(* phM) [MakeStringPair ("Spaceeship", "Asteroid")] =
& ShipasterOid;
(* phM) [MakeStringPair ("Spaceship", "SpaceStation"] =
& shipStation;
...
Return PHM;
}
} // end namespace
The Lookup function must also be modified to handle the PAIR
Namespace {// i Explain this Below - Trust ME
HitFunctionPtr Lookup (const string & class1,
Const string & class2)
{
Static auto_ptr
CollisionMap (InitializationCollisionMap ());
// See Below for a description of make_pair
Hitmap :: item MapenTry =
CollisionMap-> Find (make_pair (class1, class2));
IF (MaPENTRY == CollisionMap-> end ()) Return 0;
Return (* MapenTry). Second;
}
} // end namespace
This is almost the same as the code we wrote before. The only substantive difference is this statement that uses the Make_Pair function:
Hitmap :: item MapenTry =
CollisionMap-> Find (make_pair (class1, class2));
Make_pair is just a conversion function (template) in the standard runtime (see Item E49 and Item M35), which allows us to avoid trouble of declaring types when constructing the Pair object. We have to write this:
Hitmap :: item MapenTry =
CollisionMap-> Find (Pair
This requires multiple words to write more words, and the type of PAIR is redundant (they are the type of Class1 and Class2), so Make_Pair's form is more common.
Because MakeStringPair, INITIALIZECOLLISIONMAP and LOOKUP are declared in an unnamed namespace, and their implementations must also be in the same namespace. That's why the implementation of these functions is written in an unknown namespace (must be in the same compilation unit): This linker can correctly define their definition (or implementation) with them. The pre-declaration is associated.
We finally reached our goal. If new GaemObject's subclasses are added, the existing class does not need to recompile (unless they use new classes). There is no contrast of RTTI and the unmistable of IF ... then ... Else. Increasing a new class only needs to be clearly defined: add one or more new mapping relationships in InitializeCollisionMap, and declare a new collision process in the unnamed namespace where ProcessCollision is located. We spent a lot of effort to go to this step, but at least effort is worth it. is it? is it? Maybe.
* Inherit and simulate virtual function table
We have the last problem that needs to be handled. (If, at this point, you have the last question to handle, you will recognize the difficulty of designing a virtual function system.) Everything we do will work very well, as long as we do not need to do in calling the collision process function Type conversion to the base class map. Assuming that this game we developed must distinguish between trade spaceships and military spacecrafts. We will modify the inheritance system as follows, inheriting the entity class CommercialShip and MilitaryShip from abstract class SpaceShip.
GameObject
| | | | |
/ | /
/ | /
/ | /
/ | /
Spaceship SpaceStation Asteroid
/ /
/ /
Commercial Ship Military SHIP
Assuming that the traders and military spacecraft are consistent in the collision process. Thus, we expect to use the same collision processing function (there is also that before adding these two categories). In particular, when a MILITARYSHIP object and an Asteroid object collide, we expect to call
Void Shipasteroid (GameObject & SpaceShip,
GameObject & asteroid;
It will not be called. In fact, a UnknownCollision exception is thrown. Because Lookup finds a function in CollisionMap based on the type name "MILITARYSHIP" and "Asteroid". Although MilitaryShip can be converted to a spaceship, Lookup doesn't know this.
Moreover, there is no simple way to tell it. If you need to implement double scheduling, and you need this to map, you can only use the second virtual function call to the second virtual function (but also means adding new categories), everyone must recompile).
* Initialize the simulated virtual function table (discussed again)
This is what you want to say about dual scheduling, but it is very unpleasant to use such pessimistic terms. Therefore, let us end with both ways to initialize CollisionMap.
According to the current situation, our design is completely static. Every time we register a collision process, we have to stay with it forever. What happens if we want to increase, delete or modify a collision processing function during the game run? Not provided.
But it can be done. We can put the mapping table in a class and provide a member function that is dynamically modified by the mapping relationship. E.g:
Class CollisionMap {
PUBLIC:
Typedef void (* HitFunctionPtr) (GameObject &, GameObject &);
Void Addentry (Const String & Type1,
Const string & type2,
HitFunctionPtr CollisionFunction,
Bool symmetric = true); // See Belowvoid Removentry (Const String & Type1,
Const string & type2;
HitFunctionPtr Lookup (const string & type1,
Const string & type2;
// this function returns a reason to the one and only
// map - See Item 26
Static CollisionMap & ass ();
Private:
// Thase Functions Are Private to Prevent the Creation
// of Multiple Maps - See Item 26
CollisionMap ();
CollisionMap (Const CollisionMap &);
}
This class allows us to increase and delete the operation in the mapping table, and find the corresponding collision processing function according to the type name. It also uses tips in Item E26 to limit the number of CollisionMap objects, because there is only one mapping table in our system. (More complex games require multiple mapping tables to be imagined.) Finally, it allows us to simplify the increase in symmetry in the mapping table (that is, the type T1 object hits T2 objects and T2 objects T1 object, its effect is the same.) The process, it automatically increases the symmetrical mapping relationship, as long as AdDentry is called, the optional parameter Symmetric is set to TRUE.
With the CollisionMap class, each user who wants to increase the mapping relationship can do it directly:
Void Shipasteroid (GameObject & SpaceShip,
GameObject & asteroid;
CollisionMap :: thecollisionmap (). Addentry ("spaceship",
"Asteroid",
& ShipasterOid);
Void ShipStation (GameObject & SpaceShip,
GameObject & SpaceStation;
CollisionMap :: thecollisionmap (). Addentry ("spaceship",
"SpacStation",
& shopStation);
Void AsteroidStation (GameObject & asteroid,
GameObject & SpaceStation;
CollisionMap :: thecollisionMap (). Addentry,
"SpacStation",
& asteroidStation);
...
You must ensure that the mapping relationship is added to the mapping table before the collision occurs. A method is to make the subclass of GameObject to confirm in the constructor. This will result in a small performance overhead in the run. Another way is to create a registercollisionFunction class:
Class registercollisionfunction {
PUBLIC:
RegistercollisionFunction
Const string & type1,
Const string & type2,
CollisionMap :: HitFunctionPtr CollisionFunction,
BOOL SYMMETRIC = true)
{
CollisionMap :: ThecollisionMap (). Addentry (Type1, Type2, CollisionFunction,
Symmetric);
}
}
Users can then use this type of global object to automatically register the functions they need:
RegisterCollisionFunction CF1 ("Spaceeship", "Asteroid",
& ShipasterOid);
RegisterCollisionFunction CF2 ("Spaceeship", "SpacStation",
& shopStation);
RegisterCollisionFunction CF3 ("Asteroid", "SpaceStation",
& asteroidStation);
...
Int main (int Argc, char * argv [])
{
...
}
Because these global objects are constructed before the MAIN is called, they have entered a mapping table in the constructed function. If a derived class is added later
Class Satellite: Public GameObject {...};
And one or more collision processing functions
Void Satelliteship (GameObject & Satellite,
GameObject & SpaceShip;
Void SatelliteasterOid (GameObject & Satellite,
GameObject & asteroid;
These new functions can be added to the mapping table with the same method without the need to modify the existing code:
RegisterCollisionFunction CF4 ("Satellite", "Spaceeship",
& Satelliteship;
RegisterCollisionFunction CF5 ("Satellite", "AsteroID",
& SatelliteAnTeroid;
This will not change the fact that there is no perfect solution to achieving multiple scheduling. But it makes it easy to provide data to MAP-based implementation, if we think this implementation is closest to our needs.
* Note 11:
It is to be pointed out that it is not so confirmed. The C standard does not specify the return value of type_info :: Name, different implementations, and its behavior will be different. (For example, for a "class spaceship" for class spaceship, type_info :: name to return "Class SpaceShip".) Better design is identified by the address of the Type_info object it associated, because each class associated type_info object is definitely different. Hitmap should then be stated as MAP