将模板函数中的基础类型缓存以在std::is_base_of<Here!,>中使用

3
我希望创建一个库,实现以下功能:-
  1. 用户可以通过addCallback<Base>(Callback* callback)方法添加回调函数(通常在第一时间步骤中)。
  2. 稍后,在通常不同的.cpp文件中,当用户调用actor<Bx>()时:

    • 如果Bx继承自Base,则调用callback->callback()
    • 否则不执行任何操作
  3. (信息) 我确定每个Bx都继承自A

这是初始代码:-

#include <iostream>
class A{};
class Callback{
     public: virtual void callback()=0; 
};
template<class Base> void addCallback(Callback* callback){  
    //???
};
template<class Bx> void actor(){
    //???
}
//^^^^^^ my library end here

class B : public A{};
class B1 : public B{};
class C : public A{};
class CallbackCustom1: public Callback{
    public: virtual void callback(){std::cout<<"A"<<std::endl;}
};
class CallbackCustom2: public Callback{
    public: virtual void callback(){std::cout<<"B"<<std::endl;}
};
int main(){
    CallbackCustom1 call1;
    CallbackCustom2 call2;
    addCallback<A>(&call1); 
    addCallback<B>(&call2); 
    //vvv  below is usually in another .cpp
    actor<B1>(); // should print "A" and "B"
    actor<C>(); // should print "A" only
}

如何做到呢?

我提供的糟糕解决方案

解决方案1:std::is_base_of

我非常喜欢使用std::is_base_of<Base,Derive>
但是,这是不可能的,因为用户希望在actor<Bx>()中只调用单个类型Bx以方便使用。
std::is_base_of需要两个类的名称而不是一个。

解决方案2 (MCVE演示):虚析构函数+std::function

它可以进行更多优化,但我希望保持简单:-

#include <iostream>
#include <functional>
class A{public: virtual ~A()=default; };
class Callback{
     public: virtual void callback()=0; 
};
class MyTuple{public:
    std::function<bool(A*)> func;
    Callback* callback;
};
std::vector<MyTuple> myTuples;
template<class Base> void addCallback(Callback* callback){  
    std::function<bool(A*)> func=
        [](A* a){return dynamic_cast<Base*>(a)!=nullptr;};
    MyTuple tuple; tuple.func=func; tuple.callback=callback;
    myTuples.push_back(tuple);
}
template<class Bx> void actor(){
    Bx b;
    for(auto tuple:myTuples){
        if(tuple.func(&b)){
            tuple.callback->callback();
        }
    }
}
//^^^^^^ my library end here

它能够工作,但有一些缺点:

  • 我必须给A添加虚析构函数使其成为多态类型。我感觉这是一种不好的 hack。
  • 在我的游戏中,在某些时间步骤中,A::~A() 每秒潜在被调用 >100,000 次。
    我可以通过让 B1C 变为 final 并通过派生类进行批量删除来降低成本,但在某些地方,这是不适宜和不便的。
  • 我必须创建 Bx 的实例仅进行 dynamic_cast 检查。
    如果它的构造函数执行某些特殊操作,则可能会导致一些复杂性。

有更好的方法吗?


我想到了一个更糟糕的黑客方法:可以滥用 throwcatch 来测试非多态类之间的公共继承关系。如果你敢尝试这样做,可以使用一些 std::type_index 缓存结果,并确保每个新类型只执行一次该步骤。 - aschepler
你是否喜欢 @aschepler 在 https://dev59.com/0GMl5IYBdhLWcg3wz5uS 中提到的 Cassio Neri 的解决方案 - cppBeginner
一个相似的想法,但对于您的用例,throwcatch需要在不同的函数中。 - aschepler
1个回答

1
您能要求用户指定允许使用的Base类型集吗?如果是这样,任务就变得简单了(在线演示):
static_assert(__cplusplus >= 201703L, "example for C++17, but C++14 possible");

#include <iostream>
#include <type_traits>
#include <vector>

struct Callback {
  virtual void callback() = 0;
};

template<class... RegisteredBases>
struct CallbackSystem {

  template<class Base>
  static auto& callbacks_for() {
    static std::vector<Callback*> callbacks_for_base_{};
//
// For each `Base`, the callbacks are stored in a different vector.
// This way, we can avoid the in-loop branch (see `actor_impl`).
//
// TODO: consider performance cost of bad memory locality (can be
// improved if necessary).
//
    return callbacks_for_base_;
  }

  template<class Base>
  static void addCallback(Callback* callback) {
    static_assert((... || std::is_same<Base, RegisteredBases>{}));
    callbacks_for<Base>().push_back(callback);
  }

  template<class Derived, class RegisteredBase>
  static void actor_impl() {// called from `actor` for each RegisteredBase
    if(std::is_base_of<RegisteredBase, Derived>{}) {// branch outside loop
      // if `RegisteredBase` matches then process all its callbacks
      for(Callback* callback : callbacks_for<RegisteredBase>()) {
        callback->callback();
      }
    }
  }

  template<class Derived>
  static void actor() {
    (actor_impl<Derived, RegisteredBases>(), ...);
  }
};

允许的Base类型是这样注册的:
using MyCallbacks = CallbackSystem<A, B> {};

使用方法如下:

MyCallbacks::addCallback<A>(&call1);
MyCallbacks::addCallback<B>(&call2);
// MyCallbacks::addCallback<B1>(&call2);// compile error (good)


//vvv  below is usually in another .cpp
std::cout << R"(should print "A" and "B":)" << std::endl;
MyCallbacks::actor<B1>();

std::cout << R"(should print "A" only:)" << std::endl;
MyCallbacks::actor<C>();

另一种方法是可以反过来设计API:不限制Base类,而是要求用户指定所有允许作为actor模板参数的类。


我盯着代码看了一会儿。它很难测试,你能提供一些MVCE吗(例如http://coliru.stacked-crooked.com/)?看起来很有用。谢谢。 - cppBeginner
@cppBeginner:已添加在线演示链接(请参见我的答案的第一段)。单例模式可能是不好的设计,但约束类型的想法也可以应用于非单例:CallbackSystem<A, B> callbacks{}; callbacks.add_callback<A>(&call1); /* ...通过引用传递实例... */ callbacks.actor<C>(); - Julius
在你的第一个版本中(我更喜欢的那个),用户需要进行额外的输入。这样做有点容易出错且难以维护。(虽然比CRTP更清晰和更好。)在这个版本中,用户必须使用正确的回调来调用actor,例如CallbackSystem<A, B>::actor()。尽管有MyCallbacks别名,但回调的实用性降低了。顺便说一下,我为了MCVE的简单性而使用单例。我实际上并没有使用该模式。谢谢。 :) - cppBeginner
@cppBeginner:是的,我同意我的原始答案中的建议(用户必须指定每个类的基础)容易出错。此外,需要一定程度的元编程才能收集这些用户提供的信息。我也同意如果使用CallbackSystem<A, B>::actor<C>()而不是MyCallbacks::actor<C>();,当前版本会很脆弱。请注意,如果在非单例实例中处理回调,则不会出现此问题:void do_stuff(MyCallbacks& callbacks) { actor<C>(callbacks); } - Julius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接