31 让函数根据一个以上的对象类型来决定如何虚化

对于一个视频游戏软件,涉及到宇宙飞船、太空站,小型星是否存在碰撞的风险。先写一个继承类:

 

class GameObject{...};
class SpaceShip: public GameObject{...};
class SpaceStation: public GameObject{...};
class Asteroid: public GameObject{...};

写个函数来检测并处理对象碰撞:

void checkForCollision(GameObject& object1,GameObject& object2)
{
	if(theyJustCollided(object1,object2))
		processCollision(object1,object2);
	else
		...
}

当调用processCollision,你知道object1和object2彼此碰撞,但是你不知道这两个对象具体是什么物体,你只知道是GameObjects。如果碰撞只依据object1的动态类型决定,你可以令processCollision成为GameObjects的虚函数,然后调用object1.processCollision(object2)。如果依据object2的动态类型决定一样。但是如果同时两者的动态类型决定,这样是不行的。你需要某种函数,其行为视一个以上的对象的动态类型决定。C++没提供这样的函数。这种任务叫做“double dispatching”。更广泛的情况则称为“multiple dispatch”。

  • 虚函数 + RTTI(运行时期类型辨别)

虚函数实现一个single dispatching:声明一个collide,并在子类中重载:

class GameObject
{
public:
	virtual void collide(GameObject& otherObject) = 0;
	...
};

class SpaceShip: public GameObject
{
public:
	virtual void collide(GameObject& otherObject);
	...
};

这里只写了SpaceShip,SpaceStation和Asteroid形式完全一样。

实现double dispatching最常见的方法就是就只是在虚函数中使用if-then-else:

class CollisionWithUnkownObject{
public:
	CollisionWithUnkownObject(GameObject& whatWeHit);
	...
};

void SpaceShip::collide(GameObject& otherObject)
{
	const type_info& = typeid(otherObject);
	if(objectType == typeid(SpaceShip))
	{
		SpaceShip& ss = static_cast<SpaceShip&> (otherObject);
		process a SpaceShip-SpaceShip collision;
	}
	else if(objectType == typeid(SpaceStation))
	{
		SpaceStation& ss = static_cast<SpaceShip&> (otherObject);
		process a SpaceShip-SpaceStation collision;
	}
	else if(objectType == typeid(Asteroid)))
	{
		Asteroid& ss = static_cast<SpaceShip&> (otherObject);
		process a SpaceShip-Asteroid collision;
	}
	else
	{
		throw CollisionWithUnkownObject(otherObject);
	}
}

这里我们只检查了otherObject的类型,另一个是*this。它的类型由虚函数体系决定。我们整处于SpaceShip的成员函数,所以*this肯定是一个SpaceShip对象,上述方法没维护性,不易扩展。

  • 只是用虚函数

只是用虚函数来解决double dispatching。这个和RTTI这样的架构差不多。collide函数被声明为virtual,并被所有的派生类重新定义,此外,它还被每个类重载,每个重载处理一个派生类:

class SpaceShip;
class SpaceStation;
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 SpaceShip: public GameObject
{
public:
	virtual void collide(GameObject& otherObject);
	virtual void collide(SpaceShip& otherObject);
	virtual void collide(SpaceStation& otherObject);
	virtual void collide(Asteroid& otherObject);
};

基本想法是,将double-dispatching以两个single dispatching实现出来,其一用来决定第一个个对象的动态类型,其二用来决定第二个对象的动态类型。和先前一样,第一个虚函数调用动作针对的是“接获GameObjects&参数”的collide,实现如下:

void SpaceShip::collide(GameObject& otherObject)
{
	otherObject.collide(*this);
}

上述调用的将是参数SapceShip&类型的虚函数:

void SpaceShip::collide(SpaceShip& otherObject)
{
	process a SpaceShip-SpaceShip collision;
}
void SpaceShip::collide(SpaceStation& otherObject)
{
	process a SpaceShip-SpaceStation collision;
}
void SpaceShip::collide(Asteroid& otherObject)
{
	process a SpaceShip-Asteroid collision;
}

但是这样的代码缺点是,和上述RTTI一样,一旦有新的class加入,代码必须修改。 

  • 自行仿真虚函数表格(virtual Function Tables)

我们可以利用函数指针数组(vtbl)来实现虚函数;有两人vtbl,编译器就不用执行那么一大串if-then-else运算,并得以在所有的虚函数调用端产生相同的代码,用以(1)决定正确的vtbl索引,(2)调用vtbl中索引位置内所指的函数。

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;
}

在SpaceShip::collide函数内,我们需要一种方法,将参数otherObject的动态类型映射到某个member function指针,指向适当的碰撞处理函数,一种简单的做法产生一个关系型数组,只要获得class名称,便能导出适当的member function指针。直接使用这个数组来实现collide是有可能的,但如果加上一个中介函数lookup,会更容易。lookup获得一个GameObject并返回适当的member function指针:

class SpaceShip: public GameObject
{
private:
	typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
	static HitFunctionPtr lookup(const GameObject& whatWeHit);
	...
};

void SpaceShip::collide(GameObject& otherObject)
{
	HitFunctionPtr hfp = lookup(otherObject);
	
	if(hfp)
		(this->*hfp)(otherObject);
	else
		throw CollisionWithUnknowObject(otherObject);
}

lookup实现如下:

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
	static HitMap collisionMap;
	
	HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
	if(mapEntry == collisionMap.end()) return 0;
	
	return (*mapEntry).second;
}
  • 将自行仿真的虚函数表格(Virtual Function Tables)初始化

现在面临collisionMap初始化的问题,或许可以这么做:

//一个不正确的做法
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
	static HitMap collisionMap;
	collisionMap["SpaceShip"] = &hitSpaceShip;
	collisionMap["SpaceStation"] = &hitSpaceStation;
	collisionMap["Asteroid"] = &hitAsteroid;
	...
}

但是这会在每次lookup被调用时“将member function指针”安插到collisionMap内,这是不必要的。此外上述做法也无法编译。

此时,我们只需要的一个方法,将“member function指针”放进collisionMap内一次就好——在collisionMap诞生时刻。wo们只需要编写一个private static member function的名为initializeCollisionMap,用以产生并初始化我们的map,然后initializeCollisionMap的返回collisionMap的的初值即可:

class SpaceShip: public GameObject
{
private:
	static HitMap initializeCollisionMap();
	...
};

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
	static HitMap collisionMap = initializeCollisionMap();
	...
}

我们可以使用智能指针来代替initializeCollisionMap返回的值,这样避免了返回对象的开销,返回指针的泄露问题:

class SpaceShip: public GameObject
{
private:
	static HitMap* initializeCollisionMap();
	...
};

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
	static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
	...
}

最明直接而明显initializeCollisionMap实现方法是这样:

SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
	HitMap* phm = new HitMap;
	(*phm)["SpaceShip"] = &hitSpaceShip;
	(*phm)["SpaceStation"] = &hitSpaceStation;
	(*phm)["Asteroid"] = &hitAsteroid;

     return phm;
}

上述无法通过编译,因为函数指针的形参数不一样。为了通过编译,你可一用reinterpret_cast来“欺骗”编译器,通常在两个函数指针之间做转型的一个选择:

SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
	HitMap* phm = new HitMap;
	(*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr> (&hitSpaceShip);
	(*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation);
	(*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid);
	return phm;
}

上述转型,如果在那些函数中GamesObjects derived classes运用了多重继承或者拥有虚拟基类,会导致问题(unnatural polymorphism)。

现在只有一个办法解决冲突,将上述函数指针的参数统一为GameObject:

class GameObject
{
public:
	virtual void collide(GameObject& otherObject) = 0;
	...
};

class SpaceShip:public GameObject
{
public:
	virtual void collide(GameObject& otherObject);
	virtual void hitSpaceShip(GameObject& spaceShip);
	virtual void hitSpaceStation(GameObject& spaceShip);
	virtual void hitAsteroid(GameObject& spaceShip);
};

现在可以用希望的initializeCollisionMap形式了:

SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
	HitMap* phm = new HitMap;
	(*phm)["SpaceShip"] = &hitSpaceShip;
	(*phm)["SpaceStation"] = &hitSpaceStation;
	(*phm)["Asteroid"] = &hitAsteroid;
    return phm;
}

很可惜的是,我们将撞击的参数统一了,但是非它们所期望的精确的derived class参数,为了做到这个,我们必须在函数中使用dynamic_cast:

void SpaceShip::hitSpaceShip(GameObject& spaceShip)
{
	SpaceShip& otherShip = dynamic_cast<SpaceShip&> (spaceShip);
	process a SpaceShip-SpaceShip collision;
}

void SpaceShip::hitSpaceStation(GameObject& spaceShip)
{
	SpaceStation& otherShip = dynamic_cast<SpaceStation&> (spaceShip);
	process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(GameObject& spaceShip);
{
	Asteroid& otherShip = dynamic_cast<Asteroid&> (spaceShip);
	process a SpaceShip-Asteroid collision;
}
  • 使用“非成员(non-Member)函数”的碰撞处理函数

上述做法使用,数组指针指向的member function,所以有新的GameObject类型加入这个游戏,我们就要修改class的定义,这和单纯使用虚函数来解决double-dispatching一个道理,导致重新编译问题。如果关系数组内含的指针指向的是non-member functions,那么就可以消除这个问题,还可以解决一个设计的问题,如果两个不同的物体相撞,到底哪一个class应该负责处理?

·如果将碰撞处理函数移除classes之外,processCollision函数如下:

#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"


namespace
{
	//主要的碰撞处理函数
	void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
	void shipStation(GameObject& spaceShip,GameObject& spaceStaion);
	void asteroidStation(GameObject& asteroid,GameObject& spaceStaion);
	...

	//次要的碰撞处理函数
	//只是为了实现对称性,对调参数为主,然后调用主要的碰撞处理函数
	void asteroidShip(GameObject& asteroid,GameObject& spaceShip)
	{
		shipAsteroid(spaceShip,asteroid);
	}

	void stationShip(GameObject& spaceStaion,GameObject& spaceShip)
	{
		shipStation(spaceShip,spaceStaion);
	}

	void stationAsteroid(GameObject& spaceStaion,GameObject& asteroid)
	{
		asteroidStation(asteroid,spaceStaion);
	}
	...

	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);
}



void processCollision(GameObject& object1,GameObject& object2)
{
	HitFunctionPtr phf = lookup(typeid(object1).name(),typeid(object2).name());
	if(phf) phf(object1,object2);
	else throw UnknowCollision(object1,object2);
}

initializeCollisionMap和makeStringPair还有lookup实现如下:

namespace
{
	pair<string,string> makeStringPair(const char* s1,const char* s2)
	{
		return pair<string,string>(s1,s2);
	}
}

namespace
{
	HitMap* initializeCollisionMap()
	{
		HitMap* phm = new HitMap;
		(*phm)[makeStringPair("SpaceShip","Asteroid")]
		 = &shipAsteroid;
		 
		 (*phm)[makeStringPair("SpaceShip","SpaceStation")]
		 = &shipStation;
		 ...
		 
		 return phm;
	}
}

namespace
{
	HitFunctionPtr lookup(const string& class1,const string& class2)
	{
		static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
		
		HitMap::iterator mapEntry = 
			collisionMap->find(make_pair(class1,class2));
		
		if(mapEntry == collisionMap.end()) return 0;
		
		return (*mapEntry).second;
			
	}
}
  • 将自行仿真的虚函数表格初始化(再度讨论)

我们希望能够对撞击函数做新增、移除、修改等动作。我们可以利用一个map来存储撞击处理函数,放进某个class内,该class提供一些member functions,让我们动态修改map的内容:

class CollisionMap
{
public:
	typedef void (*HitFunctionPtr)(GameObject&,GameObject&);
	void addEntry(const string& type1,
				  const string& type2,
				   HitFunctionPtr collisionFunction,
				   bool symmtric = true);
				   
	void remove(const string& type1,const string& type2);
	
	HitFunctionPtr lookup(const string& type1,const string& type2);
	
	//仅仅只有一个map
	static CollisionMap& theCollisionMap();
private:
	//放置产生多个maps
	CollisionMap();
	CollisionMap(const CollisionMap&);

};

有了这个CollisionMap class,用户可以这样直接的伪map加上一个条目:

void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);

CollisionMap::theCollisionMap().addEntry("SpaceShip","Asteroid",
										 &shipAsteroid);
										 
void shipStation(GameObject& spaceShip,GameObject& spaceStaion);

CollisionMap::theCollisionMap().addEntry("SpaceShip","spaceStaion",
										 &shipStation);
void asteroidStation(GameObject& asteroid,GameObject& spaceStaion);

CollisionMap::theCollisionMap().addEntry("asteroid","spaceStaion",
										 &asteroidStation);

确保map在对应的任何碰撞发生之前就加入map之中。办法之一就是令GameObjects subclasses的constructors加以检查,看对象产生之际已有适当的map条目加入,还有一种做法就是产生一个人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);
	}
};

client可以利用这种类型的全局对象来自动注册它们所需的函数:

RegisterCollisionFunction cf1("SpaceShip","Asteroid",&shipAsteroid);
RegisterCollisionFunction cf2("SpaceShip","SpaceStation",&shipStation);
RegisterCollisionFunction cf3("Asteroid","SpaceStation",&asteroidStation);
...

int main()
{
	...
}

由于这些全局对象都在main之前产生的,它们的constructors所注册的函数也会在main被调用前加入map。如果有加入一个新的derived class:


class Satellite: public GameObject{...};

并写一个或多个新的碰撞处理函数:

void satelliteShip(GameObject& satellite,GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite,GameObject& asteroid);

这些新函数可以类似方法加入到map之中,不需要改动原有的代码:

RegisterCollisionFunction("Satellite","SpaceShip",&satelliteShip);
RegisterCollisionFunction("Satellite","Asteroid",&satelliteAsteroid);

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/82377400
31
31)