Как определить метод класса, который принимает указатель на функцию-член производного класса в качестве параметра в C ++? - PullRequest
2 голосов
/ 03 октября 2011

Я использую реализацию 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 для создания пользовательского обработчика событий. Но в реализации Interactor::addListener(), где я использую код Signals.h, вы должны предоставить указатель на функцию-член, а также указатель на экземпляр класса, чтобы подключить делегат к некоторому сигналу.

Ниже приведен фрагмент кода, показывающий, как я пытаюсь использовать сигналы для реализации прослушивателей событий. Я использую карту от EventType s до 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, которые, вероятно, еще не существуют?

1 Ответ

3 голосов
/ 03 октября 2011

Существует два подхода к обратным вызовам: либо вы предоставляете интерфейс с виртуальной функцией, которая будет вызываться, либо вы выполняете стирание типа , чтобы сделать его полностью универсальным. Во второй версии вы либо используете предварительно упакованное решение для 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, и он не будет DialEventListener объектом внутри addListener. Передав ссылку на базовый тип, вы можете использовать его как базу, пока объект сохраняет свою идентичность.

Это, в свою очередь, требует, чтобы объект, который вы передаете, не был временным (вы не можете использовать DialEventListener() в качестве второго аргумента для addListener, так как время жизни временного объекта закончится в конце полного выражения, и это означает, что у вас будет свисающая ссылка (в вашей текущей реализации у вас есть свисающий указатель внутри реализации сигнала, который, скорее всего, вызовет UB после срабатывания сигнала).

Кроме того, внутри addListener вы не хотите копировать сигнал, сохраненный на карте, а скорее изменять его, поэтому, опять же, вы должны использовать ссылку, а не значение при доступе к сигналу на карте.

Документация вашего класса должна быть достаточно явной, так как время жизни переданного объекта должно пережить Interactor (или разрешить отмену регистрации , чтобы клиент мог удалить обработчик из Interactor до того, как обратный вызов будет уничтожен (опять же, если вы этого не сделаете, вы закончите в UB)

Другой подход заключается в выполнении стирания типа , я никогда не использовал эту конкретную библиотеку сигналов, поэтому играю здесь на слух. Используемая вами библиотека сигналов (вероятно) реализует внутреннее стирание типов, но вам необходимо иметь возможность пересылать точный тип, пока библиотека не сможет выполнить стирание, или использовать в интерфейсе другую библиотеку стирания типов.

// 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::bind( &AnotherListener::method, &listener, _1 ); ...

...