C++ 暴露一对多关系的方法的模板与继承 在一个游戏引擎的上下文中,考虑下面的基于组件的设计: 可以有许多行为的节点类 API的用户可以创建节点实例并添加尽可能多的行为 子类,因为他们想要;通过行为子类,它们本质上 自定义游戏逻辑。然而,他们不能创造新的环境 节点的子类。他们还可以向节点询问它的任何行为 可能有

C++ 暴露一对多关系的方法的模板与继承 在一个游戏引擎的上下文中,考虑下面的基于组件的设计: 可以有许多行为的节点类 API的用户可以创建节点实例并添加尽可能多的行为 子类,因为他们想要;通过行为子类,它们本质上 自定义游戏逻辑。然而,他们不能创造新的环境 节点的子类。他们还可以向节点询问它的任何行为 可能有,c++,templates,polymorphism,C++,Templates,Polymorphism,鉴于上述情况,节点将有2种方法: addBehavior:向节点添加具体的行为类型 getBehaviors:返回来自 指定的类型,包括just Behavior,表示“所有行为类型”,可以选择按某个谓词进行筛选 我的难题是是否使用模板和常规继承来实现上述内容,因为我发现这两种方法都有缺点;我对迄今为止提出的两种方法都不太满意,我想知道是否有明确的理由让我选择一种实现而不是另一种实现 考虑以下出发点: class Behavior { public: virtual void doYou

鉴于上述情况,节点将有2种方法:

addBehavior
:向节点添加具体的行为类型

getBehaviors
:返回来自 指定的类型,包括just Behavior,表示“所有行为类型”,可以选择按某个谓词进行筛选

我的难题是是否使用模板和常规继承来实现上述内容,因为我发现这两种方法都有缺点;我对迄今为止提出的两种方法都不太满意,我想知道是否有明确的理由让我选择一种实现而不是另一种实现

考虑以下出发点:

class Behavior {
public:
    virtual void doYourThing() = 0;
};

class Jump : public Behavior {
public:
    virtual void doYourThing() {
        //  Jump!
    }
};

class Run : public Behavior {
public:
    virtual void doYourThing() {
        //  Run at runSpeed!
    }

    int runSpeed;
};

class Node {
    //  addBehavior:
    //  getBehaviors:
private:
    unordered_map<type_index, vector<Behavior*>> behaviorsM;
};
到目前为止还不错。但是考虑一下,如果我给<代码>节点< /代码>添加了一些行为,稍后我想设置任何<代码>运行< /代码>行为的运行速度(并且我对行为不再有我自己的参考,否则我只需要通过那些类型化的参考工具设置它们)。情况是这样的:

Jump j1;
Run r1;
Run r2;

node.addBehavior(j1);
node.addBehavior(r1);
node.addBehavior(r2);

//  Later on... no longer have the refs j1,r1,r2, so I need to query the node:
for (Behavior* behavior : node.getBehaviors(typeid(Run))) {
    static_cast<Run*>(behavior)->runSpeed = 20;
}
跳转j1;
运行r1;
运行r2;
节点行为(j1);
addBehavior(r1);
addBehavior(r2);
//后来。。。不再具有参考j1、r1、r2,因此我需要查询节点:
for(Behavior*Behavior:node.getBehaviors(typeid(Run))){
静态(行为)->运行速度=20;
}
好人:

  • 写一次,可以用于任何行为
  • 可以放在实现文件中,只留下声明 在节点的接口文件中
坏消息:

  • 这个东西的用户将不得不强制转换为行为子类型,如果它们 需要对该子类型执行任何特定的操作,即使它们是 保证他们得到该特定客户的所有行为 亚型;如果他们想要使用谓词函数,则相同
所以

通用方法:

void addBehavior(Behavior& behavior) {
    behaviorsM[typeid(behavior)].push_back(&behavior);
}

template<class ElementType>
using Predicate = function<bool(ElementType& e, bool& stop)>;

const vector<Behavior*> getBehaviors(ClassType type = typeid(Behavior), Predicate<Behavior> predicate = nullptr) {
    vector<Behavior*> unfilteredBehaviors;
    if (type == typeid(Behavior)) {
        for (auto pair : behaviorsM) {
            vector<Behavior*>& behaviors = behaviorsM[pair.first];
            unfilteredBehaviors.insert(end(unfilteredBehaviors), begin(behaviors), end(behaviors));
        }
    }
    else {
        unfilteredBehaviors = behaviorsM[type];
    }

    if (!predicate) {
        predicate = [](const Behavior& b, bool& stop){return true;};
    }

    bool stop = false;
    vector<Behavior*> results;
    for (auto behavior : unfilteredBehaviors) {
        if(predicate(*behavior, stop)) {
            results.push_back(behavior);
            if (stop) {
                break;
            }
        }
    }

    return results;
}
template <class BehaviorType,
typename enable_if<
is_base_of<Behavior, BehaviorType>::value &&
!is_abstract<BehaviorType>::value,
BehaviorType>::type* = nullptr>
void addBehavior(BehaviorType& behavior) {
    behaviorsM[typeid(BehaviorType)].push_back(&behavior);
}

template<class ElementType>
using Predicate = function<bool(ElementType& e, bool& stop)>;

template <class BehaviorType,
typename enable_if<
is_base_of<Behavior, BehaviorType>::value,
BehaviorType>::type* = nullptr>
const vector<BehaviorType*> getBehaviors(Predicate<BehaviorType> predicate = nullptr) {
    vector<Behavior*> unfilteredBehaviors;
    type_index requestedType = typeid(BehaviorType);
    if (requestedType == typeid(Behavior)) {
        for (auto pair : behaviorsM) {
            vector<Behavior*>& behaviors = behaviorsM[pair.first];
            unfilteredBehaviors.insert(end(unfilteredBehaviors), begin(behaviors), end(behaviors));
        }
    }
    else {
        unfilteredBehaviors = behaviorsM[requestedType];
    }

    if (!predicate) {
        predicate = [](const BehaviorType& b, bool& stop){return true;};
    }

    bool stop = false;
    vector<BehaviorType*> results;
    for (auto behavior : unfilteredBehaviors) {
        BehaviorType* concreteBehavior = static_cast<BehaviorType*>(behavior);
        if(predicate(*concreteBehavior, stop)) {
            results.push_back(concreteBehavior);
            if (stop) {
                break;
            }
        }
    }

    return results;
}
template::type*=nullptr>
void addBehavior(行为类型和行为){
behaviorsM[typeid(BehaviorType)]。推回(&behavior);
}
模板
使用谓词=函数;
模板::类型*=nullptr>
常量向量getBehaviors(谓词=nullptr){
向量未过滤行为;
类型\索引请求类型=类型ID(行为类型);
if(requestedType==typeid(行为)){
for(自动配对:behaviorsM){
向量和行为=behaviorsM[pair.first];
插入(结束(未过滤行为)、开始(行为)、结束(行为));
}
}
否则{
unfilteredBehaviors=behaviorsM[requestedType];
}
if(!谓词){
谓词=[](const BehaviorType&b,bool&stop){return true;};
}
bool-stop=false;
矢量结果;
用于(自动行为:未筛选的行为){
BehaviorType*concreteBehavior=静态(行为);
if(谓词(*concreteBehavior,stop)){
结果:推回(具体行为);
如果(停止){
打破
}
}
}
返回结果;
}
嗯。。让我们看看。现在我可以做到:

for (Run* behavior : node.getBehaviors<Run>()) {
    behavior->runSpeed = 20;
}
for(运行*行为:node.getBehaviors()){
行为->运行速度=20;
}
顺便说一句,如果我觉得更具描述性,我也可以这样做:

node.addBehavior<Jump>(j1);
node.addBehavior<Run>(r1);
node.addBehavior<Run>(r2);
node.addBehavior(j1);
addBehavior(r1);
addBehavior(r2);
但在我看来,这并不是免费的。在我看来:

好人:

  • 用户获得了一个已经转换到请求的子类型的行为向量,这似乎与人们抽象地思考“让我所有跳转行为”的方式一致。就用户体验而言,这在我看来是一个很大的优势
  • 写一次,用于任何类型。。。某种程度上。。看到坏的一面
坏消息:

  • 就我个人而言,我喜欢将头文件视为类的公共和受保护的API引用。如果我把模板定义放在头文件中,我觉得我已经污染了本应该是一个包含所有实现细节的简短“接口文件”
  • 如果我将模板定义放在实现文件中,我必须对每个行为类型进行显式实例化,无论是在cpp中还是在cpp将包含的单独文件中。基本上违背了“编写一次,用于任何类型”的目的。这是因为这个想法是,这个东西的用户将不断创造新的行为。要求他们还为每个行为向某个文件添加create和get显式实例化,这太糟糕了。我想我可以添加一些代码生成脚本来自动添加它们,但是。。这一切让我有点难过
  • 实际上,编译器实际上是为每个类型创建每个方法的一个版本,这也让我有点难过,因为考虑到模板化方法的情况,而不是整个模板化类,在这种情况下,类模板作为创建类类型的“蓝图”是有意义的
  • 最后,用户不必强制转换,但在内部,模板实现实际上强制转换了行为的每个实例

因此,底线是让用户进行强制转换感觉很糟糕,模板版本为类的客户端提供了更好的用户体验,但代价是大量的东西。因此,我不确定这是否是模板的有效用途,也不确定它是否具有所有缺点。

关于各种行为,您不能将它们实现为?@vsoftco策略如何使“getBehaviors”返回的元素可以作为具体子类型访问,而无需强制转换,但同时避免所有的tem