首页 > 解决方案 > 多态性和函数绑定

问题描述

对于我正在编写的事件系统,我想将回调绑定到函数列表。

这是我想做的一个基本示例:

#include <iostream>
#include <functional>
#include <string>

class Base {
  public:
    virtual std::string getType() const = 0;
};

class Derived : public Base {
  protected:
    int some_data;

  public:

    Derived(int some_data): some_data(some_data) {}

    virtual std::string getType() const {
      return "Derived";
    }

    int getData() const {
      return this->some_data;
    }
};

class DerivedTwo : public Base {
  protected:
    double some_data;

  public:

    DerivedTwo(double some_data): some_data(some_data) {}

    virtual std::string getType() const {
      return "DerivedTwo";
    }

    // The type of data is not always the same.
    double getData() const {
      return this->some_data;
    }
};

// The type of member should ALWAYS be Derived but then i can't store it in <callback>
void onDerivedEvent(Base& member) {
  std::cout << member.getType() << std::endl;

  // This is obviously not possible with member being a base class object
  // member.getData();
}

// The type of member should ALWAYS be DerivedTwo but then i can't store it in <callback>
void onDerivedTwoEvent(Base& member) {
  std::cout << member.getType() << std::endl;
}

int main() {
  std::function<void(Base&)> callback;

  callback = std::bind(onDerivedEvent, std::placeholders::_1);
  callback(Derived(2));

  callback = std::bind(onDerivedTwoEvent, std::placeholders::_1);
  callback(DerivedTwo(3.0));

  return 0;
}

我唯一想改变的是onCallback()应该将派生类成员作为参数而不是对基对象的引用,所以我可以调用getData()例如。
在本例中,这意味着:

void onCallback(Derived& derived);

但是,如果我这样做,我将无法再bind()使用该方法,callback因为参数类型不匹配。

有谁知道如何使这项工作?

// 编辑
对不起,这里的混乱,我用一些更多的细节和例子更新了源代码,以澄清我在这里做什么。

注意:
因为这看起来非常相关,所以这里是我在这里尝试做的具体用例:
它是我正在构建的引擎的事件系统的一部分。有预定义的基本事件,但它应该可以由使用此引擎的用户使用更具体的事件进行扩展。所以没有明确的派生类列表。然后某个对象可以订阅特定的事件类型,并且每当中央事件总线接收到这样的事件时,它都会以事件作为参数调用所有订阅的回调函数。我没有在派生类中添加一个和所有句柄函数的原因是,事件可以以多种方式使用。

对评论中的一些问题的回答:

如果您将 onCallback 传递给不是特定的 Derived& 的对象,会发生什么?(即,添加一个具有 doStuff2 的 Derived2。将其传递给回调。你想发生什么?那应该是不可能的。

我可能没有对此进行澄清,并且在开始时也有误导性信息,从那时起我就对其进行了编辑。传递的派生类的类型总是事先知道的。例如:onKeyEvent将始终接收 KeyEvent 对象,而不是基类对象或任何其他派生变体。
但是,此函数绑定到的变量应该能够存储接受不同派生类的函数Base

这是我对所有事件的存储:

std::map<EventType, std::list<std::function<void(const Event&)>>> listener_map;

为什么 onCallback 不是 Derived 覆盖的 Base 方法

我在评论中回答了这个问题。...The reason i am not adding a one and for all handle function in the derived class is, the events an be used in multiple ways...
意思是,我可能有KeyEvent一个键(哪个键,它是按下/释放/保持)的数据,并且监听功能可以将这些数据用于任何它想要的。(检查是否按下了某个特定键,检查是否按下了任何随机键等等。)其他一些事件可能根本没有任何数据,只是通知侦听器发生了某些事情或有多组数据等。

在您的代码中的任何一点,是否存在或是否存在一个有限的、有界的、在编译时从 Base 派生的所有类型的中央列表?

理论上是的。在编译期间将有有限数量的派生类。但是,对于库的编译和使用此库的项目的编译,这些可能会有所不同。

标签: c++bindingpolymorphism

解决方案


template<class Base>
struct poly_callback {
  template<class T>
  static poly_callback make( std::function<void(T&)> f ) {
    return { std::function<void(void*)>( [f]( void* ptr ) { f(*static_cast<T*>(static_cast<Base*>(ptr))); }) };
  }
  template<class T>
  poly_callback( void(*pf)(T&) ):poly_callback( make<T>( pf ) ) {}
  poly_callback( poly_callback const& ) = default;
  poly_callback( poly_callback && ) = default;
  void operator()( Base& b ) {
    return type_erased( static_cast<void*>(std::addressof(b)) );
  }
private:
  std::function<void(void*)> type_erased;
  poly_callback( std::function<void(void*)> t ):type_erased(std::move(t)) {}
};

Apoly_callback<Event>可以存储具有与 兼容的签名的可调用对象void(Derived&),其中Derived派生自Event。当它盲目地向下转换时,必须使用类型的确切实例Derived&或未定义的行为结果来调用它。

停止使用std::bind,它在功能上已过时。

class Base {
  public:
    virtual std::string getType() const = 0;
};

class Derived : public Base {
  protected:
    int some_data;

  public:

    Derived(int some_data): some_data(some_data) {}

    virtual std::string getType() const {
      return "Derived";
    }

    int getData() const {
      return this->some_data;
    }
};

class DerivedTwo : public Base {
  protected:
    double some_data;

  public:

    DerivedTwo(double some_data): some_data(some_data) {}

    virtual std::string getType() const {
      return "DerivedTwo";
    }

    // The type of data is not always the same.
    double getData() const {
      return this->some_data;
    }
};

// The type of member should ALWAYS be Derived but then i can't store it in <callback>
void onDerivedEvent(Derived& member) {
  std::cout << member.getType() << "\n";
  std::cout << member.getData() << "\n";
}

// The type of member should ALWAYS be DerivedTwo but then i can't store it in <callback>
void onDerivedTwoEvent(DerivedTwo& member) {
  std::cout << member.getType() << "\n";
  std::cout << member.getData() << "\n";
}

struct callbacks {
    std::unordered_map< std::string, std::vector< poly_callback<Base> > > events;
    void invoke( std::string const& name, Base& item ) {
        auto it = events.find(name);
        if (it == events.end())
            return;
        for (auto&& f : it->second)
            f( item );
    }
    template<class Derived>
    void connect( std::string const& name, void(*pf)(Derived&) )
    {
        events[name].push_back( pf );
    }
    template<class Derived>
    void connect_T( std::string const& name, std::function<void(Derived&)> f )
    {
        events[name].push_back( std::move(f) );
    }
};

int main() {
  callbacks cb;
  cb.connect("one", onDerivedEvent );
  cb.connect("two", onDerivedTwoEvent );

  Derived d(7);
  DerivedTwo d2(3.14);

  cb.invoke( "one", d );
  cb.invoke( "two", d2 );

  return 0;
}

活生生的例子

可以针对安全性和可用性进行调整。例如,检查 typeid 是否实际匹配。

输出是:

Derived
7
DerivedTwo
3.14

如您所见,回调函数采用Derived&DerivedTwo&对象。


以我的经验,这是一个糟糕的计划。

相反,broadcaster<KeyboardEvent> keyboard;请不要使用字符串查找您的事件注册表系统。

仅当有某种方法可以统一处理回调时,从字符串到回调的映射才有意义。而且您不想统一对待这些回调。即使您为了效率而选择统一存储它们(在可笑的巨大框架中很有用),我也想要类型安全的 API 而不是映射。


推荐阅读