Удобно пользоваться уже готовыми событиями из каких-нибудь сторонних библиотек, Boost.Signals например. Но если тебя ограничивают лишь стандартом, то приходится выдумывать свой велосипед, так как механизм событий в C++ стандартом не поддерживается.
Классический способ реализации подобного поведения - шаблон Наблюдатель.
Примеров использования этого шаблона навалом. Я приведу один из них.
Ну на самом деле выдумывать свой велосипед не очень то и надо. Как реализовать события на стандартном C++ гуглится на раз-два. Возьмем хотя бы этот пример. Ниже слегка адаптированный вариант
В приведенном примере есть один большой недостаток - TResult. Это видно из метода CppEvent::notify
Собственно, а зачем нам тогда вообще ResultT? Я вырезал это ограничение, заменив типом void.
Есть еще один существенный, на мой взгляд, недостаток у приведенного примера. Что будет, если какой-либо подписчик будет использовать один метод для обработки событий из многих источников? Подписчик никогда не узнает, какой источник прислал ему событие. В некоторых
случаях это может оказаться полезным, и лишать подписчика такой возможности нельзя. Я добавил в шаблон события параметр SenderT.
Что получилось
Во-первых, почему для отписки от события нужно передавать какой-то id? Если передать значение меньше id, но больше 0, то можно кого-то и отписать от события.
Во-вторых, где очищается память, которая занимается новыми EventHandler? Я предлагаю все заботы о создании, хранении и удалении переложить EventHandler на подписчиков. По идее это обязанности подписчиков, потому что только они знают, когда нужно добавить новый обработчик, а когда нужно его удалить. Таким образом, событие должно получать для подписки/отписки именно обработчик.
В итоге получим следующий класс события.
Мне, как разработчику в основном использующему C#, кажется, что не очень хорошо, когда при подписке нужно обязательно указывать экземпляр подписчика. Но что делать, от этого пока никуда не деться. Компромиссное решение - выделить интерфейсы получателя и подписчика, что и сделано в примере.
1. Обработчики не имеют возвращаемых значений.
2. Через событие передается и источник события.
Кстати, получившийся класс события рассчитан только на один параметр. Я считаю, что этого вполне достаточно, чтобы передавать любую информацию из источника. Так, например, и сделано в .NET.
На самом деле новый стандарт C++11 предоставляет более гибкие возможности для реализации событий. Например, лямбда-выражения и std::function могут существенно облегчит жизнь разработчику. Но это уже другая история.
Классический способ реализации подобного поведения - шаблон Наблюдатель.
Примеров использования этого шаблона навалом. Я приведу один из них.
В этом примере есть недоработки, но, думаю, суть всем ясна.
- class Observer
- {
- public:
- void update(int i)
- {
- std::cout << i;
- std::cin.get();
- }
- };
- class Subject
- {
- private:
- std::list<Observer*> _observers;
- int _count;
- public:
- Subject()
- : _count(0)
- {}
- void Attach(Observer* obs)
- {
- _observers.assign(1, obs);
- }
- void Detach(Observer* obs)
- {
- _observers.remove(obs);
- }
- void commit(int i)
- {
- std::list<Observer*>::iterator it = _observers.begin();
- for(; it != _observers.end(); it++)
- {
- (*it)->update(i);
- }
- }
- };
* This source code was highlighted with Source Code Highlighter.
Ну на самом деле выдумывать свой велосипед не очень то и надо. Как реализовать события на стандартном C++ гуглится на раз-два. Возьмем хотя бы этот пример. Ниже слегка адаптированный вариант
Что нового в этом коде по сравнению с использованием Наблюдателя? Сделали отдельный класс события, который теперь реализует всю логику работы с подпиской, отпиской и уведомлением. Да, теперь для подписки нужен не только сам класс Наблюдателя, нужно еще указывать конкретный его метод.
- #include "stdafx.h"
- #include <map>
- #include <iostream>
- template <typename ReturnT,typename ParamT>
- class EventHandlerBase
- {
- public:
- virtual ReturnT notify(ParamT param) = 0;
- };
- template <typename ListenerT,typename ReturnT,typename ParamT>
- class EventHandler : public EventHandlerBase<ReturnT,ParamT>
- {
- typedef ReturnT (ListenerT::*PtrMember)(ParamT);
- ListenerT* m_object;
- PtrMember m_member;
- public:
- EventHandler(ListenerT* object, PtrMember member)
- : m_object(object), m_member(member)
- {}
- ReturnT notify(ParamT param)
- {
- return (m_object->*m_member)(param);
- }
- };
- template <typename ReturnT,typename ParamT>
- class CppEvent
- {
- typedef std::map<int,EventHandlerBase<ReturnT,ParamT> *> HandlersMap;
- HandlersMap m_handlers;
- int m_count;
- public:
- CppEvent()
- : m_count(0) {}
- template <typename ListenerT>
- int attach(ListenerT* object,ReturnT (ListenerT::*member)(ParamT))
- {
- typedef ReturnT (ListenerT::*PtrMember)(ParamT);
- m_handlers[m_count] = (new EventHandler<ListenerT,
- ReturnT,ParamT>(object,member));
- m_count++;
- return m_count-1;
- }
- bool detach(int id)
- {
- HandlersMap::iterator it = m_handlers.find(id);
- if(it == m_handlers.end())
- return false;
- delete it->second;
- m_handlers.erase(it);
- return true;
- }
- ReturnT notify(ParamT param)
- {
- HandlersMap::iterator it = m_handlers.begin();
- for(; it != m_handlers.end(); it++)
- {
- it->second->notify(param);
- }
- return true;
- }
- };
- class Listener
- {
- public:
- bool update_int(int p)
- {
- std::cout << p;
- std::cin.get();
- return true;
- }
- };
- class Subject
- {
- public:
- CppEvent<bool,int> int_event;
- void submit_int(int i)
- {
- int_event.notify(i);
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Listener* l = new Listener();
- Subject* s = new Subject();
- s->int_event.attach(l, &Listener::update_int);
- s->submit_int(345);
- delete l;
- delete s;
- return 0;
- }
* This source code was highlighted with Source Code Highlighter.
В приведенном примере есть один большой недостаток - TResult. Это видно из метода CppEvent::notify
Во-первых, непонятно, почему этот метод должен возвращать RetrunT. Во-вторых, что означает true, которое он будет возвращать. Все это пользователь почувствует, если попытается декларировать событие с возвращаемым значением, которое нельзя привести к BOOL. Для себя я исправил этот метод так
- ReturnT notify(ParamT param)
- {
- HandlersMap::iterator it = m_handlers.begin();
- for(; it != m_handlers.end(); it++)
- {
- it->second->notify(param);
- }
- return true;
- }
* This source code was highlighted with Source Code Highlighter.
Так как все равно все возвращаемые значения обработчиков будут теряться, я принял соглашение, что все обработчики будут возвращать void.
- void notify(ParamT param)
- {
- HandlersMap::iterator it = m_handlers.begin();
- for(; it != m_handlers.end(); it++)
- {
- it->second->notify(param);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Собственно, а зачем нам тогда вообще ResultT? Я вырезал это ограничение, заменив типом void.
Есть еще один существенный, на мой взгляд, недостаток у приведенного примера. Что будет, если какой-либо подписчик будет использовать один метод для обработки событий из многих источников? Подписчик никогда не узнает, какой источник прислал ему событие. В некоторых
случаях это может оказаться полезным, и лишать подписчика такой возможности нельзя. Я добавил в шаблон события параметр SenderT.
Что получилось
Недостатков на самом деле осталось полно.
- template <typename SenderT,typename ParamT>
- class EventHandlerBase
- {
- public:
- virtual void notify(SenderT *sender, ParamT param) = 0;
- };
- template <typename ListenerT, typename SenderT, typename ParamT>
- class EventHandler: public EventHandlerBase<SenderT, ParamT>
- {
- typedef void (ListenerT::*PtrMember)(SenderT*, ParamT);
- ListenerT* m_object;
- PtrMember m_member;
- public:
- EventHandler(ListenerT* object, PtrMember member)
- : m_object(object), m_member(member)
- {}
- void notify(SenderT* sender, ParamT param)
- {
- (m_object->*m_member)(sender, param);
- }
- };
- template <typename SenderT,typename ParamT>
- class CppEvent
- {
- typedef std::map<int,EventHandlerBase<SenderT, ParamT> *> HandlersMap;
- HandlersMap m_handlers;
- int m_count;
- public:
- CppEvent()
- : m_count(0) {}
- template <typename ListenerT>
- int attach(ListenerT* object, void (ListenerT::*member)(SenderT *, ParamT))
- {
- typedef void (ListenerT::*PtrMember)(SenderT*, ParamT);
- m_handlers[m_count] = (new EventHandler<ListenerT,
- SenderT,ParamT>(object,member));
- m_count++;
- return m_count-1;
- }
- bool detach(int id)
- {
- HandlersMap::iterator it = m_handlers.find(id);
- if(it == m_handlers.end())
- return false;
- delete it->second;
- m_handlers.erase(it);
- return true;
- }
- void notify(SenderT* sender, ParamT param)
- {
- HandlersMap::iterator it = m_handlers.begin();
- for(; it != m_handlers.end(); it++)
- {
- it->second->notify(sender, param);
- }
- }
- };
* This source code was highlighted with Source Code Highlighter.
Во-первых, почему для отписки от события нужно передавать какой-то id? Если передать значение меньше id, но больше 0, то можно кого-то и отписать от события.
Во-вторых, где очищается память, которая занимается новыми EventHandler? Я предлагаю все заботы о создании, хранении и удалении переложить EventHandler на подписчиков. По идее это обязанности подписчиков, потому что только они знают, когда нужно добавить новый обработчик, а когда нужно его удалить. Таким образом, событие должно получать для подписки/отписки именно обработчик.
В итоге получим следующий класс события.
Как использовать эти события, показывает следующий пример.
- template <typename SenderT,typename ParamT>
- class CppEvent
- {
- private:
- std::list<EventHandlerBase<SenderT, ParamT> *> m_handlers;
- public:
- template <typename ListenerT>
- inline bool attach(EventHandler<ListenerT, SenderT, ParamT>* handler)
- {
- m_handlers.assign(1, handler);
- return true;
- }
- template <typename ListenerT>
- inline bool detach(EventHandler<ListenerT, SenderT, ParamT>* handler)
- {
- m_handlers.remove(handler);
- return true;
- }
- void notify(SenderT* sender, ParamT param)
- {
- std::list<EventHandlerBase<SenderT, ParamT> *>::iterator it = m_handlers.begin();
- for(; it != m_handlers.end(); it++)
- {
- (*it)->notify(sender, param);
- }
- }
- };
* This source code was highlighted with Source Code Highlighter.
Получилось в итоге что-то похожее на события в .NET.
- class IListener;
- class ISubject;
- class ISubject
- {
- public:
- virtual void Attach(EventHandler<IListener, ISubject, int>* handler) = 0;
- virtual void Detach(EventHandler<IListener, ISubject, int>* handler) = 0;
- };
- class IListener
- {
- public:
- virtual void updated(ISubject* subject, int i) = 0;
- };
- class Subject: public ISubject
- {
- CppEvent<ISubject, int> event;
- public:
- inline virtual void Attach(EventHandler<IListener, ISubject, int>* handler)
- {
- event.attach(handler);
- }
- inline virtual void Detach(EventHandler<IListener, ISubject, int>* handler)
- {
- event.detach(handler);
- }
- void submit()
- {
- event.notify(this, 2);
- }
- };
- class Listener: public IListener
- {
- EventHandler<IListener, ISubject, int>* _handler;
- ISubject * _subject;
- public:
- Listener(ISubject *s)
- {
- _subject = s;
- _handler = new EventHandler<IListener, ISubject, int>(this, &IListener::updated);
- s->Attach(_handler);
- }
- virtual void updated(ISubject* subject, int i)
- {
- std::cout << i <<std::endl;
- std::cin.get();
- }
- ~Listener()
- {
- _subject->Detach(_handler);
- delete _handler;
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Subject* s = new Subject();
- Listener* l = new Listener(s);
- s->submit();
- delete l;
- delete s;
- return 0;
- }
* This source code was highlighted with Source Code Highlighter.
Мне, как разработчику в основном использующему C#, кажется, что не очень хорошо, когда при подписке нужно обязательно указывать экземпляр подписчика. Но что делать, от этого пока никуда не деться. Компромиссное решение - выделить интерфейсы получателя и подписчика, что и сделано в примере.
Заключение
Наш же пример показывает, что удобно, когда1. Обработчики не имеют возвращаемых значений.
2. Через событие передается и источник события.
Кстати, получившийся класс события рассчитан только на один параметр. Я считаю, что этого вполне достаточно, чтобы передавать любую информацию из источника. Так, например, и сделано в .NET.
На самом деле новый стандарт C++11 предоставляет более гибкие возможности для реализации событий. Например, лямбда-выражения и std::function могут существенно облегчит жизнь разработчику. Но это уже другая история.
Для реализации Наблюдателя удобно использовать signals или signals2 (поддержка многопоточности) из boost. Или (если boost использовать нельзя) создать свою реализацию на их основе. В boost обработчики могут иметь возвращаемые значения, поддерживаются приоритеты, параметров обработчика может быть несколько, возможность автоматического отсоединения сигнала от слота при удалении объекта, обработчики не обязаны быть методами класса и т.д.
ОтветитьУдалитьЛямда-выражения это просто функторы создаваемые "на лету", а function обеспечивает вызов только одного "обобщенного" делегата т.е. при реализации событий они представляют меньший интерес, чем signals.
Пример 1 будет работать криво (например: _observers.assign(1, obs) - обозреватель всегда будет 1; зачем здесь _count? и т.д.). В последнем примере то же: m_handlers.assign(1, handler). Последний пример не безопасен относительно исключений: надо использовать RAII. Проблемы также возникнут, если Subject будет удален раньше, чем Listener и т.д.
Таким образом, ИМХО, предложеная реализация требует доработки для использования в реальных приложениях или для того, чтобы стать хорошим примером.
Я же сказал, что удобно пользоваться уже готовыми решениями. См. первый абзац. Ну а если делать события свои, то не обязательно делать это на основе boost. Все зависит от поставленной задачи. Я показал, как добиться какой-то базовой функциональности. Если кому-нибудь нужно что-то сверх, бери и делай.
УдалитьПо поводу примера 1. Почему обозреватель будет 1? А count это да - забыл убрать.
Буду признателен, если ты приведешь код, как в моем примере использовать RAII. Про исключения, это ты верно заметил.
Тем не менее, я считаю этот пример хорошим. Он верно передает суть многих реализаций событий.
В приведенном коде, кстати, еще много "дыр". Просто нет времени досконально все проверять. Например, "интерфейс" ISubject/IListener не годен для серьезного использования (поскольку он error prone) т.к. не имеет виртуального деструктора. С++ это не С#: здесь интерфейсов нет, есть только абстрактные классы.
Удалить"По поводу примера 1. Почему обозреватель будет 1?" - И в последнем примере тоже обозреватель будет 1. Потому, что: m_handlers.assign(1, handler) в функции attach сначала удаляет все элементы из list, а потом копирует в него handler в количестве 1. По поводу RAII: использовать умные указатели shared_ptr/weak_ptr/unique_ptr в зависимости от контекста, код приводить лень. Это также (при правильном использовании) решит важную проблему твоего решения (цитирую из своего пред. поста): "Проблемы также возникнут, если Subject будет удален раньше, чем Listener".
УдалитьP.S. Кстати, забавный факт: weak_ptr является своего рода наблюдателем shared_ptr.