们也知道了怎么将映射表的实现细节封装在lookup函数中。因为这张表包含的是指向成员函数的指针,所以在增加新的GameObject类型时仍然需要修改类的定义,这还是意味着所有人都必须重新编译, 即使他们根本不关心这个新的类型。 例如, 如果增加了一个Satellite类型, 我们不得不在SpaceShip类中增加一个处理SpaceShip和Satellite对象间碰撞的函数。所有SpaceShip的用户不得不重新编译,即使他们根本不在乎Satellite对象的存在。这个问题将导致我们否决只使用虚函数来实现二重调度,解决方法是只需做小小的修改。
如果映射表中包含的指针指向非成员函数,那么就没有重编译问题了。而且,转到非
成员的碰撞处理函数将让我们发现一个一直被忽略的设计上的问题,就是,应该在哪个类里处理不同类型的对象间的碰撞?在前面的设计中,如果对象1和对象2碰撞,而正巧对象1是processCollision的左边的参数,碰撞将在对象1的类中处理;如果对象2正巧是左边的参数,碰撞就在对象2的类中处理。这个有特别的含义吗?是不是这样更好些:类型A和类型B的对象间的碰撞应该既不在A中也不在B中处理, 而在两者之外的某个中立的地方处理?
任何碰撞处理函数。我们可以将实现碰撞处理函数的文件组织成这样:
#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 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);
}
注意,用了无名的命名空间来包含实现碰撞处理函数所需要的函数。无名命名空间中
的东西是当前编译单元(其实就是当前文件)私有的--很象被申明为文件范围内static
的函数一样。有了命名空间后,文件范围内的static已经不赞成使用了,你应该尽快让自
己习惯使用无名的命名空间(只要编译器支持) 。
理论上,这个实现和使用成员函数的版本是相同的,只有几个轻微区别。第一,
HitFunctionPtr现在是一个指向非成员函数的指针类型的重定义。第二,意料之外的类
CollisionWithUnknownObject被改叫UnknownCollision,第三,其构造函数需要两个对象作参数而不再是一个了。这也意味着我们的映射需要三个消息了:两个类型名,一个
HitFunctionPtr。
// 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
namespace { // unnamed namespace again — see below
pair<string,string> makeStringPair(const char *s1, const char *s2) {
return pair<string,string>(s1, s2);
}
} // end namespace
namespace { // still the unnamed namespace — see below HitMap * initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid;
(*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
...
return phm;
}
} // end namespace
lookup函数也必须被修改以处理pair<string,string>对象,并将它作为映射表的第
一部分:
namespace { // I explain this below — trust me
HitFunctionPtr lookup(const string& class1, const string& class2)
{
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
// see below for a description of make_pair
HitMap::iterator mapEntry= collisionMap->find(make_pair(class1, class2));
if (mapEntry == collisionMap->end())
return 0;
return (*mapEntry).second;
}
} // end namespace
因为makeStringPair、initializeCollisionMap和lookup都是申明在无名的命名空
间中的,它们的实现也必须在同一命名空间中。这就是为什么这些函数的实现在上面被写在了一个无名命名空间中的原因(必须和它们的申明在同一编译单元中) :这样链接器才能正确地将它们的定义(或说实现)与它们的前置申明关联起来。
我们最终达到了我们的目的。如果增加了新的GaemObject的子类,现存类不需要重新
编译(除非它们用到了新类) 。没有了RTTI的混乱和if...then...else的不可维护。增加
一个新类只需要做明确定义了的局部修改:在initializeCollisionMap中增加一个或多个
新的映射关系, 在processCollision所在的无名的命名空间中申明一个新的碰撞处理函数。
我们花了很大的力气才走到这一步,但至少努力是值得的。是吗?是吗? 也许吧。