我正在为我正在开发的小游戏创建一个面向组件的系统。基本结构如下:游戏中的每个对象都由一个“GameEntity”组成;一个容器,其中包含指向“Component”类中项目的指针。
组件和实体通过调用组件的父GameEntity类中的send方法相互通信。 send方法是具有两个参数的模板,Command(包括诸如STEP_TIME之类的指令的枚举)和泛型类型'T'的数据参数。 send函数循环遍历Component *向量并调用每个组件的接收消息,由于模板的使用,可以方便地调用与数据类型T对应的重载接收方法。
然而,问题出现(或者更确切地说是不便之处)是,Component类是纯虚函数并且将始终被扩展。由于不允许模板函数被虚拟化的实际限制,我必须在标题中为每个数据类型声明一个虚拟接收函数,这可以被组件使用。这不是非常灵活,也不是可扩展的,而且至少对我而言似乎违反了不必要地复制代码的OO编程精神。
所以我的问题是,如何修改下面提供的代码存根,使我的组件定向对象结构尽可能灵活,而不使用违反最佳编码实践的方法
以下是每个类的相关标头存根以及可能使用扩展组件类的示例,以便为我的问题提供一些上下文:
游戏实体类:
class Component;
class GameEntity
{
public:
GameEntity(string entityName, int entityID, int layer);
~GameEntity(void){};
//Adds a pointer to a component to the components vector.
void addComponent (Component* component);
void removeComponent(Component*);
//A template to allow values of any type to be passed to components
template<typename T>
void send(Component::Command command,T value){
//Iterates through the vector, calling the receive method for each component
for(std::vector<Component*>::iterator it =components.begin(); it!=components.end();it++){
(*it)->receive(command,value);
}
}
private:
vector <Component*> components;
};
组件类: #include“GameEntity.h” class Component
{
public:
static enum Command{STEP_TIME, TOGGLE_ANTI_ALIAS, REPLACE_SPRITE};
Component(GameEntity* parent)
{this->compParent=parent;};
virtual ~Component (void){};
GameEntity* parent(){
return compParent;
}
void setParent(GameEntity* parent){
this->compParent=parent;
}
virtual void receive(Command command,int value)=0;
virtual void receive(Command command,string value)=0;
virtual void receive(Command command,double value)=0;
virtual void receive(Command command,Sprite value)=0;
//ETC. For each and every data type
private:
GameEntity* compParent;
};
Component类的可能扩展:
#include "Sprite.h"
#include "Component.h"
class GraphicsComponent: Component{
public:
GraphicsComponent(Sprite sprite, string name, GameEntity* parent);
virtual void receive(Command command, Sprite value){
switch(command){
case REPLACE_SPRITE: this->sprite=value; break
}
}
private:
Spite sprite;
}
我应该使用空指针并将其转换为适当的类型吗?这可能是可行的,因为在大多数情况下,该类型将从命令中获知,但同样不是很灵活。
答案 0 :(得分:3)
这是类型擦除的完美案例!
当基于模板的通用编程和面向对象的编程发生冲突时,你会遇到一个简单但难以解决的问题:如何以安全的方式存储一个我不关心的变量输入但是关心我如何使用它?通用编程往往会导致类型信息的爆炸式增长,而面向对象的编程则依赖于非常具体的类型。程序员要做什么?
在这种情况下,最简单的解决方案是某种容器,它具有固定的大小,可以存储任何变量,并且SAFELY检索它/查询它的类型。幸运的是,boost有这样一种类型:boost::any。
现在你只需要一个虚拟功能:
virtual void receive(Command command,boost::any val)=0;
每个组件“知道”它发送的内容,因此可以取出值,如下所示:
virtual void receive(Command command, boost::any val)
{
// I take an int!
int foo = any_cast<int>(val);
}
这将成功转换int,或抛出异常。不喜欢异常吗?先做一个测试:
virtual void receive(Command command, boost::any val)
{
// Am I an int?
if( val.type() == typeid(int) )
{
int foo = any_cast<int>(val);
}
}
“但是哦!”你可能会说,渴望不喜欢这个解决方案,“我想发送多个参数!”
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<double, char, std::string>) )
{
auto foo = any_cast< std::tuple<double, char, std::string> >(val);
}
}
“嗯”,你可能会思考,“我如何允许传递任意类型,就像我想要浮动一次和另一个?”对此,先生,你会被打败,因为这是一个坏主意。而是将两个入口点捆绑到同一个内部对象:
// Inside Object A
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<double, char, std::string>) )
{
auto foo = any_cast< std::tuple<double, char, std::string> >(val);
this->internalObject->CallWithDoubleCharString(foo);
}
}
// Inside Object B
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<float, customtype, std::string>) )
{
auto foo = any_cast< std::tuple<float, customtype, std::string> >(val);
this->internalObject->CallWithFloatAndStuff(foo);
}
}
你有它。通过使用boost :: any删除类型的讨厌“有趣”部分,我们现在可以安全,安全地传递参数。
有关类型擦除的更多信息,以及在不需要的对象上擦除类型部分的好处,因此它们与通用编程更好地匹配,请参阅this article
另一个想法,如果你喜欢字符串操作,就是:
// Inside Object A
virtual void receive(Command command, unsigned int argc, std::string argv)
{
// Use [boost::program_options][2] or similar to break argv into argc arguments
// Left as exercise for the reader
}
这有一种奇特的优雅;程序以相同的方式解析它们的参数,因此您可以将数据消息传递概念化为运行“子程序”,然后打开一大堆隐喻,这样可能会导致有趣的优化,例如线程化部分数据消息等等。
然而,成本很高:与简单演员相比,字符串操作可能非常昂贵。另请注意,boost :: any不是零成本;与仅传递固定数量参数所需的零查找相比,每个any_cast都需要RTTI查找。灵活性和间接性需要成本;在这种情况下,它是值得的。
如果您希望完全避免任何此类成本,则有一种可能性可以获得必要的灵活性以及无依赖性,甚至可能是更可口的语法。但虽然它是标准功能,但它可能非常不安全:
// Inside Object A
virtual void receive(Command command, unsigned int argc, ...)
{
va_list args;
va_start ( args, argc );
your_type var = va_arg ( args, your_type );
// etc
va_end( args );
}
例如,在printf中使用的变量参数功能允许您传递任意多个参数;很明显,你需要告诉被调用函数传递了多少个参数,所以这是通过argc提供的。但请记住,被调用者函数无法判断是否传递了正确的参数;它会愉快地接受你给它的任何东西并将它解释为好像是正确的。因此,如果您不小心传递了错误信息,则不会有编译时间支持来帮助您找出问题所在。垃圾进,垃圾出。
此外,还有一些关于va_list的东西需要记住,例如所有浮动都被上转换为double,结构通过指针传递(我认为),但如果你的代码是正确和精确的,那么就没有问题了你将具有效率,缺乏依赖性和易用性。对于大多数用途,我建议将va_list等包装到宏中:
#define GET_DATAMESSAGE_ONE(ret, type) \
do { va_list args; va_start(args,argc); ret = va_args(args,type); } \
while(0)
然后是两个args的版本,然后是三个版本。遗憾的是,这里不能使用模板或内联解决方案,但是大多数数据包的参数不会超过1-5个,而且大多数都是原语(几乎可以肯定,虽然你的用例可能不同),所以设计一些丑陋的宏来帮助你的用户在很大程度上处理不安全的问题。
我不推荐这种策略,但它可能是某些平台上最快速,最简单的策略,例如不允许编译时依赖性或嵌入式系统,其中虚拟调用可能是不允许的。