【C++ techniques】让函数根据一个以上的对象类型来决定如何虚化

写一个视频游戏软件,涉及到宇宙飞船、太空站、小行星等天体:

cpp 复制代码
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
		...
}

碰撞无法同时依据object1和object2两者的动态类型决定;你需要的是某种函数,其行为视一个以上的对象的动态类型决定,C++并未提供这样的函数,并不直接支持"double dispatching"/"multiple dispatch"。

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

cpp 复制代码
//声明一个collide,并在子类中重载:
class GameObject
{
public:
	virtual void collide(GameObject& otherObject) = 0;
	...
};
 
class SpaceShip: public GameObject
{
public:
	virtual void collide(GameObject& otherObject);
	...
};

最一般化的double dispatching实现法:利用if-then-else来仿真虚函数:

cpp 复制代码
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);
	}
}

缺点:造成程序难以维护。

只使用虚函数

使用虚函数实现double dispatching:

将double-dispatching以两个single dispatching(也就是两个分离的虚函数调用)实现出来:其一用来决定第一对象的动态类型,其二用来决定第二对象的动态类型。和先前一样,第一个虚函数调用动作针对的是"接获GameObjects&参数"的collide函数:

cpp 复制代码
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);
	...
};

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

//上述实现调用的是:
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;
}

缺点:一旦有新的class加入,程序代码必须从头到尾修改。

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

回顾:
【C++ Efficiency】理解虚函数、多重继承、虚基类和RTTI

编译器通常通过函数指针数组vtbl)来实现虚函数,当某个函数被调用时,编译器便索引至该数组内,取得一个函数指针。有了vtbl,编译器便可以不必执行一大串if-then-else运算,并得以在所有的虚函数调用端产生相同代码,用以:

  1. 决定正确的vtbl索引;
  2. 调用vtbl中的索引位置内所指的函数。
cpp 复制代码
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;
}
cpp 复制代码
//中介函数lookup
//接获一个GameObject并返回适当的成员函数指针

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)初始化

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

//将"member function指针"放进collisionMap内一次就好
//在collisionMap诞生时刻:
class SpaceShip: public GameObject
{
private:
	static HitMap* initializeCollisionMap();
	...
};
 
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
	static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
	//使用智能指针来代替initializeCollisionMap返回的值,避免资源泄露问题:
	...
}
cpp 复制代码
//函数指针的参数均设为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);
};

//使用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)函数"的碰撞处理函数

cpp 复制代码
#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);
}

继承+自行仿真的虚函数表格

扩大继承体系时,必须要求每个人都重新编译。

将自行仿真的虚函数表格初始化(再度讨论)

cpp 复制代码
//利用一个map来存储撞击处理函数
//放进某个class内,该class提供一些成员函数,动态修改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);
	
	static CollisionMap& theCollisionMap();
	
private:
	CollisionMap();
	CollisionMap(const CollisionMap&);
};

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之中
//办法之一是令GameObject's subclasses的constructors加以检查
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( int argc, char *argv[])
{
	...
}

//如果加入一个新的派生类:
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);

还是没有完美的办法可以实现double dispatch,但此方法可以轻视完成一个以map为基础的实现品。

相关推荐
Prejudices2 分钟前
C++如何调用Python脚本
开发语言·c++·python
单音GG5 分钟前
推荐一个基于协程的C++(lua)游戏服务器
服务器·c++·游戏·lua
SoraLuna12 分钟前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷15 分钟前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿16 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
wyh要好好学习19 分钟前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech19 分钟前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#
qing_04060321 分钟前
C++——多态
开发语言·c++·多态
孙同学_22 分钟前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
九圣残炎22 分钟前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode