【问题标题】:How I do define a class method that accepts a derived class member function pointer as a parameter in C++?如何在 C++ 中定义一个接受派生类成员函数指针作为参数的类方法?
【发布时间】:2011-10-03 18:49:03
【问题描述】:

我正在使用此处找到的 Signal-Slot C++ 实现:https://github.com/pbhogan/Signals 在我正在开发的用户界面库中实现事件侦听器。

面向用户的 API 如下所示:

class DialEventListener : public EventListener {

    void handleEvent(Event event){
        …
        cout << "Event on" << event.source.name << end;
    }
}
int main(){
    Interactor dial1;
    dial1.addListener(ROTATE_EVENT, DialEventListener());
    … 
}

要定义事件侦听器,用户将继承EventListener 以创建自定义事件处理程序。但是在我使用 Signals.h 代码的Interactor::addListener() 的实现中,您必须提供一个成员函数指针以及一个类实例指针来将一个 Delegate 连接到某个 Signal。

下面是一段代码 sn-p,展示了我如何尝试使用 Signals 来实现事件侦听器。我使用从EventTypes 到Signal 的映射来说明触发多种类型事件的交互器。

typedef map< EventType, Signal1<Event> > EventTypeListenerMap;
class Interactor {

private:
   EventTypeListenerMap listener_map;

public:
   void TriggerEvent(Event e){
       Signal1<Event> signal = listener_map[e.type];
       signal(e);
   }

   void addListener(EventType e_type, EventListener listener){
      …
          //Find the signal for a particular kind of event.
      Signal1<TUIKitEvent> signal = listener_map[e_type];
          //Plug a delegate into that signal's slot
      signal.Connect( &listener, &EventListener::handleEvent); 
   }
   void addListener(EventType e_type, EventListener listener, void (EventListener::*handler)(Event)){
      …
      Signal1<TUIKitEvent> signal = listener_map[e_type];
      signal.Connect( &listener, handler); 
   }

}    

class EventListener  {
    virtual void handleEvent(Event event){
    }
}

我使用 Signal-Slot 实现的事实应该对用户隐藏,所以我只是编写了这个版本的 Interactor::addListener(EventType, EventListener, void (EventListener::*)) 作为健全性检查,以确保我传递了正确的成员函数指针以进行绑定到那个信号。但是成员函数指针是特定于类的,那么如何定义一个具有签名的函数,该函数接受指向从 EventListener 派生的类的方法的指针,而这些方法可能还不存在?

【问题讨论】:

  • 我认为问题不是很清楚...为什么要将成员函数指针传递给派生方法,而不仅仅是将成员函数传递给基类型并具有动态dispatch 为你解决最终的覆盖?也许您应该添加一小段代码,指出用户代码的外观以及您希望从中获得的行为...

标签: c++ templates function-pointers observer-pattern


【解决方案1】:

有两种回调方法,或者您提供一个接口,该接口具有一个将被调用的虚函数,或者您执行 type-erasure 以使其完全通用。在第二个版本中,您可以为 type-erasure 使用预打包的解决方案,或者您需要使用一些模板来自己实现它。

您在第一个块中显示的预期用户代码遵循第一种方法,足以满足基本需求。但请注意,您可能希望在按值传递的几个地方使用指针或引用:

// user code
Interactor dial;
DialEventListener dial_listener;
dial.addListener( ROTATE_EVENT, dial_listener );

// implementation
void Interactor::addListener( EventType event, EventListener & listener ) {
   Signal1<TUIKitEvent> & sgn = listener_map[event];
   sgn.Connect( &listener, &EventListener::handleEvent );
};

注意几个变化:listener 参数是通过引用而不是值传递的。如果按值传递,您将得到 slicing(只会复制 EventListener 子对象,它不会是 addListener 内的 DialEventListener 对象。通过传递对基类型的引用,您可以在对象保持其身份的同时将其用作基础。

这反过来又要求您传入的对象不是临时对象(您不能使用DialEventListener() 作为addListener 的第二个参数,因为临时对象的生命周期将在完整表达式的末尾结束,这意味着您将有一个悬空引用(在您当前的实现中,您在信号实现中有一个悬空指针,一旦触发信号,这很可能会导致 UB)。

此外,在addListener 内部,您不想复制 存储在地图中的信号,而是修改它,因此,在访问地图上的信号。

你的类的文档应该非常明确,因为传入的对象的生命周期必须比Interactor 长(或者允许注销,以便客户端可以从Interactor 在回调被销毁之前(同样,如果你不这样做,你将在 UB 结束)

另一种方法是执行 type-erasure,我从来没有使用过那个特定的信号库,所以我在这里靠耳朵玩。您正在使用的信号库(可能)在内部实现了类型擦除,但您需要能够转发确切的类型,直到该库可以执行擦除,或者在接口中使用不同的类型擦除库。

// Transferring the exact type to the signal lib:
class Interactor {
// ...
public:
   template <typename Listener>
   void addListener( EventType event, Listener & listener, void (Listener::*callback)(Event) ) {
      Signal1<TUIKitEvent> & sgn = listener_map[event];
      sgn.connect( &listener, callback );
   }
//...

根据信号实现中connect 方法的语义,在接口中使用不同类型的擦除库可能会也可能不会。如果它复制了第一个参数,那么您可能可以使用std::function

class Interactor {
//...
public:
   void addListener( EventType event, std::function< void (Event) > callback ) {
       Signal1<TUIKitEvent> & sgn = listener_map[event];
       sgn.connect( callback, &std::function<void(Event)>::operator() );
   }
//...
};

假设库将复制第一个参数并在内部维护它自己的副本(即假设它的行为类似于boost::signal 库)。在用户方面,他们必须执行类型擦除:

struct AnotherListener {   // no need to explicitly derive from EventListner now!
   void method( Event e ) {}
};
int main() {
   Interactor dial;
   AnotherEventListener listener;
   dial.addListener( dial, std::bind( &AnotherListener::method, &listener ) );
}

bind 的确切语法可能不是那样,我从来没有在 boost 库的新标准中使用过它,它本来应该是 boost::bind( &amp;AnotherListener::method, &amp;listener, _1 );...

【讨论】:

  • 两种方法都有效。我最初的问题是我没有通过引用传递监听器,这就是为什么它调用 EventListener::handleEvent() 而不是 DialEventListener::handleEvent()。我偶然发现了类似于第二种方法的东西,我创建了一个函数模板,如下所示:template&lt;class T, class U&gt;addListener(EventType, T listener, void (U::handleEvent)(Event))。感谢您的回答。
  • 我觉得有点有趣的是,拥有一个执行类型擦除的库(从而使用户无需实现精确的接口),您只需用一种不太灵活的方法来包装它......
  • 这可能是我最终消除的其他一些错误,但是库的擦除有两个类参数,并且由于成员函数指针是特定于类的,这可能是我的问题:请参阅template&lt; class X, class Y &gt; void Connect( Y * obj, void (X::*func)() ) { delegateList.insert( MakeDelegate( obj, func ) ); }
  • 正如我所说,我不知道那个库,但我以前使用过boost::signal 库(它已更改为boost::signal2 IIRC)并且它允许的内容非常灵活(通过将库与 boost::bind 结合起来(在 C++11 中,您可以使用 std::bind)...只是说...
猜你喜欢
  • 1970-01-01
  • 2017-01-08
  • 1970-01-01
  • 2020-08-02
  • 1970-01-01
  • 2017-07-18
  • 1970-01-01
  • 2012-04-21
  • 1970-01-01
相关资源
最近更新 更多