C++ 面向组件系统中的灵活数据消息传递

C++ 面向组件系统中的灵活数据消息传递,c++,visual-c++,components,game-engine,generic-programming,C++,Visual C++,Components,Game Engine,Generic Programming,我正在为我正在开发的一个小游戏创建一个面向组件的系统。基本结构如下:游戏中的每个对象都由一个“游戏实体”组成;包含指向“Component”类中项目的指针向量的容器 组件和实体通过在组件的父GameEntity类中调用send方法相互通信。send方法是一个模板,它有两个参数,一个命令(它是一个枚举,包括诸如STEP_TIME之类的指令)和一个泛型类型“T”的数据参数。send函数通过Component*向量循环并调用每个组件的receive消息,由于模板的使用,该消息方便地调用了重载的rece

我正在为我正在开发的一个小游戏创建一个面向组件的系统。基本结构如下:游戏中的每个对象都由一个“游戏实体”组成;包含指向“Component”类中项目的指针向量的容器

组件和实体通过在组件的父GameEntity类中调用send方法相互通信。send方法是一个模板,它有两个参数,一个命令(它是一个枚举,包括诸如STEP_TIME之类的指令)和一个泛型类型“T”的数据参数。send函数通过Component*向量循环并调用每个组件的receive消息,由于模板的使用,该消息方便地调用了重载的receive方法,该方法对应于数据类型T

然而,问题在于(或者更确切地说是不便之处),组件类是一个纯虚拟函数,并且将始终被扩展。由于不允许对模板函数进行虚拟化的实际限制,我必须在头中为每个组件可能使用的数据类型声明一个虚拟接收函数。这既不灵活,也不可扩展,而且至少在我看来,这违反了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 "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;


}

我应该使用空指针并将其转换为适当的类型吗?这可能是可行的,因为在大多数情况下,可以通过命令知道类型,但也不是很灵活。

这是一个完美的类型擦除案例

当基于模板的泛型编程和面向对象编程发生冲突时,您将面临一个简单但难以解决的问题:如何以安全的方式存储一个不关心类型而是关心如何使用它的变量?泛型编程往往会导致类型信息爆炸,而面向对象编程依赖于非常特定的类型。程序员应该做什么

在这种情况下,最简单的解决方案是某种具有固定大小的容器,可以存储任何变量,并安全地检索/查询其类型。幸运的是,boost有这样一种类型:

现在您只需要一个虚拟函数:

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);
}
这有一种奇特的优雅;程序以同样的方式解析其参数,因此您可以将数据消息传递概念化为运行“子程序”,这就打开了一系列隐喻,这可能会导致有趣的优化,例如将部分数据消息传递线程化,等等

但是,成本很高:与简单的强制转换相比,字符串操作可能非常昂贵。还要注意boost::any并不是零成本的;与只传递固定数量的参数所需的零查找相比,每个\u转换都需要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,还有很多事情需要记住,比如所有的float都被上转换为double,结构通过指针传递(我认为),但是如果代码是正确的和精确的,那么就不会有问题,并且效率高,缺乏依赖性,易于使用。对于大多数用途,我建议将va_列表等包装到宏中:

#define GET_DATAMESSAGE_ONE(ret, type) \
    do { va_list args; va_start(args,argc); ret = va_args(args,type); } \
    while(0)
然后是两个参数的版本,然后是三个参数的版本。遗憾的是,这里不能使用模板或内联解决方案,但大多数数据包的参数不会超过1-5个,而且大多数数据包都是原语(几乎可以肯定,尽管您的用例可能有所不同),因此设计一些难看的宏来帮助您的用户解决问题将在很大程度上解决不安全问题


我不推荐这种策略,但在某些平台上,它可能是最快、最简单的策略,例如不允许编译时依赖关系的平台或嵌入式系统,在这些平台上,虚拟调用可能是不允许的。

这是一个完美的类型擦除案例

当基于模板的泛型编程和面向对象编程发生冲突时,您将面临一个简单但难以解决的问题:如何以安全的方式存储一个不关心类型而是关心如何使用它的变量?泛型编程往往会导致类型信息爆炸,而面向对象编程依赖于非常特定的类型。程序员应该做什么

在这种情况下,最简单的解决方案是某种具有固定大小的容器,可以存储任何变量,并安全地检索/查询其类型。幸运的是,boost有这样一种类型:

现在哟
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);
    }
}
// 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
}
// 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 );
}
#define GET_DATAMESSAGE_ONE(ret, type) \
    do { va_list args; va_start(args,argc); ret = va_args(args,type); } \
    while(0)