模板与继承,用于暴露一对多关系的方法

时间:2015-11-17 00:46:43

标签: c++ templates polymorphism

在各种游戏引擎的上下文中,请考虑以下基于组件的设计:

  1. 可以有许多行为的Node类。
  2. API的用户可以创建节点实例并添加尽可能多的行为 他们想要的子类;通过行为子类他们本质上 自定义游戏逻辑。然而,他们不能创造新的 Node的子类。他们也可以向Node询问任何行为 可能有。
  3. 鉴于上述情况,Node将有两种方法:

    addBehavior:向Node添加具体的行为类型。

    getBehaviors:从a的节点返回所有行为 指定的类型,包括行为,意思是“所有行为类型”,可选择通过某个谓词进行过滤。

  4. 我的困境是是否使用模板与常规继承来实现上述,因为我发现两种方法都存在缺陷;到目前为止,我对这两种方法都不满意,我不知道是否有任何明确的原因可以解释为什么我会将一种实现方式放在另一方面。

    考虑以下起点:

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

    现在是实施addBehaviorgetBehavior

    的部分

    继承方法:

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

    到目前为止一切顺利。但是考虑一下我是否向Node添加了一些行为,之后我想设置它可能具有的任何Run行为的runSpeed(并且我不再拥有自己对行为的引用;否则我只是通过那些打字的参考设置它们。情况将是这样的:

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

    好处:

    • 写一次,可以用于任何行为。
    • 可以放在实现文件中,只留下声明 在Node的接口文件中。

    坏事:

    • 如果这些东西的用户必须转换为行为子类型 需要对该子类做任何特别的事情,即使它们是 保证他们获得特定的所有行为 亚型;如果他们想使用谓词函数,也一样。

    因此...

    “通用”方法:

    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;
    }
    
    嗯......好吧,让我们看看。现在我可以这样做:

    for (Run* behavior : node.getBehaviors<Run>()) {
        behavior->runSpeed = 20;
    }
    

    如果我觉得它更具描述性,我也可以这样做:

    node.addBehavior<Jump>(j1);
    node.addBehavior<Run>(r1);
    node.addBehavior<Run>(r2);
    

    但我认为这不是免费的。我看待它的方式:

    好处:

    • 用户获取已经投射到所请求的子类型的行为向量,这似乎与人们用抽象术语思考“让我所有的跳跃行为”的方式一致。就用户体验而言,这在我看来是一个很大的优势。
    • 写一次,用于任何类型......有点......看坏。

    坏事:

    • 就个人而言,我喜欢将头文件视为类的公共和受保护的API引用。如果我将模板定义放在头文件中,我觉得我已经污染了应该是一个简短的'接口文件',其中包含所有实现细节。
    • 如果我将模板定义放在实现文件中,我必须对每个行为类型进行显式实例化,无论是在cpp中还是在cpp将包含的单独文件中。基本上打败了“一次写入,用于任何类型”的目的。这是因为这个想法的用户会不断创建新的行为。要求他们还为每个行为添加创建并获取某些文件的显式实例化。我想我可以添加一些代码生成脚本自动添加它们,但仍然......这一切让我有点难过。
    • 在现实中,编译器实际上是创建每个方法的一个版本为每种类型的,这也让我有点伤感想想一个模板方法的情况下,相对于整个模板类,在这使它感觉类模板是创建类类型的“蓝图”。
    • 最后,用户可以免于投射,但在内部,模板实现实际上会投射每一个行为实例!

    所以最重要的是,让用户做演员表会感觉很糟糕,模板版本为班级的客户提供了更好的用户体验,但代价是一堆东西。所以我不确定这是否适用于模板,如果它有所有缺点。

0 个答案:

没有答案