понедельник, 2 апреля 2012 г.

События в C++03

Удобно пользоваться уже готовыми событиями из каких-нибудь сторонних библиотек, Boost.Signals например. Но если тебя ограничивают лишь стандартом, то приходится выдумывать свой велосипед, так как механизм событий в C++ стандартом не поддерживается.

Классический способ реализации подобного поведения - шаблон Наблюдатель.
Примеров использования этого шаблона навалом. Я приведу один из них.

  1. class Observer
  2. {
  3. public:
  4.   void update(int i)
  5.   {
  6.     std::cout << i;
  7.     std::cin.get();
  8.   }
  9. };
  10.  
  11. class Subject
  12. {
  13. private:
  14.   std::list<Observer*> _observers;
  15.   int _count;
  16. public:
  17.  
  18.   Subject()
  19.     : _count(0)
  20.   {}
  21.  
  22.   void Attach(Observer* obs)
  23.   {
  24.     _observers.assign(1, obs);
  25.   }
  26.  
  27.   void Detach(Observer* obs)
  28.   {
  29.     _observers.remove(obs);
  30.   }
  31.  
  32.   void commit(int i)
  33.   {
  34.     std::list<Observer*>::iterator it = _observers.begin();
  35.     for(; it != _observers.end(); it++)
  36.     {
  37.       (*it)->update(i);
  38.     }
  39.   }
  40. };
* This source code was highlighted with Source Code Highlighter.
В этом примере есть недоработки, но, думаю, суть всем ясна.
Ну на самом деле выдумывать свой велосипед не очень то и надо. Как реализовать события на стандартном C++ гуглится на раз-два. Возьмем хотя бы этот пример. Ниже слегка адаптированный вариант

  1. #include "stdafx.h"
  2. #include <map>
  3. #include <iostream>
  4.  
  5. template <typename ReturnT,typename ParamT>
  6. class EventHandlerBase
  7. {
  8. public:
  9.   virtual ReturnT notify(ParamT param) = 0;
  10. };
  11.  
  12. template <typename ListenerT,typename ReturnT,typename ParamT>
  13. class EventHandler : public EventHandlerBase<ReturnT,ParamT>
  14. {
  15.   typedef ReturnT (ListenerT::*PtrMember)(ParamT);
  16.   ListenerT* m_object;
  17.   PtrMember m_member;
  18.  
  19. public:
  20.  
  21.   EventHandler(ListenerT* object, PtrMember member)
  22.     : m_object(object), m_member(member)
  23.   {}
  24.  
  25.   ReturnT notify(ParamT param)
  26.   {
  27.     return (m_object->*m_member)(param);
  28.   }
  29. };
  30.  
  31. template <typename ReturnT,typename ParamT>
  32. class CppEvent
  33. {
  34.   typedef std::map<int,EventHandlerBase<ReturnT,ParamT> *> HandlersMap;
  35.   HandlersMap m_handlers;
  36.   int m_count;
  37.  
  38. public:
  39.  
  40.  
  41.   CppEvent()
  42.     : m_count(0) {}
  43.  
  44.   template <typename ListenerT>
  45.   int attach(ListenerT* object,ReturnT (ListenerT::*member)(ParamT))
  46.   {
  47.     typedef ReturnT (ListenerT::*PtrMember)(ParamT);
  48.     m_handlers[m_count] = (new EventHandler<ListenerT,
  49.                 ReturnT,ParamT>(object,member));
  50.     m_count++;
  51.     return m_count-1;
  52.   }
  53.  
  54.   bool detach(int id)
  55.   {
  56.     HandlersMap::iterator it = m_handlers.find(id);
  57.  
  58.     if(it == m_handlers.end())
  59.       return false;
  60.  
  61.     delete it->second;
  62.     m_handlers.erase(it);
  63.     return true;
  64.   }
  65.  
  66.   ReturnT notify(ParamT param)
  67.   {
  68.     HandlersMap::iterator it = m_handlers.begin();
  69.     for(; it != m_handlers.end(); it++)
  70.     {
  71.       it->second->notify(param);
  72.     }
  73.  
  74.     return true;
  75.   }
  76. };
  77.  
  78. class Listener
  79. {
  80. public:
  81.  
  82.   bool update_int(int p)
  83.   {
  84.     std::cout << p;
  85.     std::cin.get();
  86.     return true;
  87.   }
  88. };
  89.  
  90. class Subject
  91. {
  92. public:
  93.  
  94.   CppEvent<bool,int> int_event;
  95.  
  96.   void submit_int(int i)
  97.   {
  98.     int_event.notify(i);
  99.   }
  100. };
  101.  
  102. int _tmain(int argc, _TCHAR* argv[])
  103. {
  104.   Listener* l = new Listener();
  105.   Subject* s = new Subject();
  106.  
  107.   s->int_event.attach(l, &Listener::update_int);
  108.  
  109.   s->submit_int(345);
  110.  
  111.   delete l;
  112.   delete s;
  113.   return 0;
  114. }
* This source code was highlighted with Source Code Highlighter.
Что нового в этом коде по сравнению с использованием Наблюдателя? Сделали отдельный класс события, который теперь реализует всю логику работы с подпиской, отпиской и уведомлением. Да, теперь для подписки нужен не только сам класс Наблюдателя, нужно еще указывать конкретный его метод.
В приведенном примере есть один большой недостаток - TResult. Это видно из метода CppEvent::notify

  1. ReturnT notify(ParamT param)
  2. {
  3.   HandlersMap::iterator it = m_handlers.begin();
  4.   for(; it != m_handlers.end(); it++)
  5.   {
  6.     it->second->notify(param);
  7.   }
  8.  
  9.   return true;
  10. }
* This source code was highlighted with Source Code Highlighter.
Во-первых, непонятно, почему этот метод должен возвращать RetrunT. Во-вторых, что означает true, которое он будет возвращать. Все это пользователь почувствует, если попытается декларировать событие с возвращаемым значением, которое нельзя привести к BOOL. Для себя я исправил этот метод так

  1. void notify(ParamT param)
  2. {
  3.   HandlersMap::iterator it = m_handlers.begin();
  4.   for(; it != m_handlers.end(); it++)
  5.   {
  6.     it->second->notify(param);
  7.   }
  8. }
* This source code was highlighted with Source Code Highlighter.
Так как все равно все возвращаемые значения обработчиков будут теряться, я принял соглашение, что все обработчики будут возвращать void.
Собственно, а зачем нам тогда вообще ResultT? Я вырезал это ограничение, заменив типом void.
Есть еще один существенный, на мой взгляд, недостаток у приведенного примера. Что будет, если какой-либо подписчик будет использовать один метод для обработки событий из многих источников? Подписчик никогда не узнает, какой источник прислал ему событие. В некоторых
случаях это может оказаться полезным, и лишать подписчика такой возможности нельзя. Я добавил в шаблон события параметр SenderT.
Что получилось

  1. template <typename SenderT,typename ParamT>
  2. class EventHandlerBase
  3. {
  4. public:
  5.   virtual void notify(SenderT *sender, ParamT param) = 0;
  6. };
  7.  
  8. template <typename ListenerT, typename SenderT, typename ParamT>
  9. class EventHandler: public EventHandlerBase<SenderT, ParamT>
  10. {
  11.   typedef void (ListenerT::*PtrMember)(SenderT*, ParamT);
  12.   ListenerT* m_object;
  13.   PtrMember m_member;
  14.  
  15. public:
  16.  
  17.   EventHandler(ListenerT* object, PtrMember member)
  18.     : m_object(object), m_member(member)
  19.   {}
  20.  
  21.   void notify(SenderT* sender, ParamT param)
  22.   {
  23.     (m_object->*m_member)(sender, param);
  24.   }
  25. };
  26.  
  27. template <typename SenderT,typename ParamT>
  28. class CppEvent
  29. {
  30.   typedef std::map<int,EventHandlerBase<SenderT, ParamT> *> HandlersMap;
  31.   HandlersMap m_handlers;
  32.   int m_count;
  33.  
  34. public:
  35.  
  36.  
  37.   CppEvent()
  38.     : m_count(0) {}
  39.  
  40.   template <typename ListenerT>
  41.   int attach(ListenerT* object, void (ListenerT::*member)(SenderT *, ParamT))
  42.   {
  43.     typedef void (ListenerT::*PtrMember)(SenderT*, ParamT);
  44.     m_handlers[m_count] = (new EventHandler<ListenerT,
  45.                 SenderT,ParamT>(object,member));
  46.     m_count++;
  47.     return m_count-1;
  48.   }
  49.  
  50.   bool detach(int id)
  51.   {
  52.     HandlersMap::iterator it = m_handlers.find(id);
  53.  
  54.     if(it == m_handlers.end())
  55.       return false;
  56.  
  57.     delete it->second;
  58.     m_handlers.erase(it);
  59.     return true;
  60.   }
  61.  
  62.   void notify(SenderT* sender, ParamT param)
  63.   {
  64.     HandlersMap::iterator it = m_handlers.begin();
  65.     for(; it != m_handlers.end(); it++)
  66.     {
  67.       it->second->notify(sender, param);
  68.     }
  69.   }
  70. };
* This source code was highlighted with Source Code Highlighter.
Недостатков на самом деле осталось полно.
Во-первых, почему для отписки от события нужно передавать какой-то id? Если передать значение меньше id, но больше 0, то можно кого-то и отписать от события.
Во-вторых, где очищается память, которая занимается новыми  EventHandler? Я предлагаю все заботы о создании, хранении и удалении переложить  EventHandler на подписчиков. По идее это обязанности подписчиков, потому что только они знают, когда нужно добавить новый обработчик, а когда нужно его удалить. Таким образом, событие должно получать для подписки/отписки именно обработчик.
В итоге получим следующий класс события.

  1. template <typename SenderT,typename ParamT>
  2. class CppEvent
  3. {
  4. private:
  5.   std::list<EventHandlerBase<SenderT, ParamT> *> m_handlers;
  6.  
  7. public:
  8.  
  9.   template <typename ListenerT>
  10.   inline bool attach(EventHandler<ListenerT, SenderT, ParamT>* handler)
  11.   {
  12.     m_handlers.assign(1, handler);
  13.     return true;
  14.   }
  15.  
  16.   template <typename ListenerT>
  17.   inline bool detach(EventHandler<ListenerT, SenderT, ParamT>* handler)
  18.   {
  19.     m_handlers.remove(handler);
  20.     return true;
  21.   }
  22.  
  23.   void notify(SenderT* sender, ParamT param)
  24.   {
  25.     std::list<EventHandlerBase<SenderT, ParamT> *>::iterator it = m_handlers.begin();
  26.     for(; it != m_handlers.end(); it++)
  27.     {
  28.       (*it)->notify(sender, param);
  29.     }
  30.   }
  31. };
* This source code was highlighted with Source Code Highlighter.
Как использовать эти события, показывает следующий пример.

  1. class IListener;
  2. class ISubject;
  3.  
  4. class ISubject
  5. {
  6. public:
  7.   virtual void Attach(EventHandler<IListener, ISubject, int>* handler) = 0;
  8.   virtual void Detach(EventHandler<IListener, ISubject, int>* handler) = 0;
  9. };
  10.  
  11. class IListener
  12. {
  13. public:
  14.   virtual void updated(ISubject* subject, int i) = 0;
  15. };
  16.  
  17. class Subject: public ISubject
  18. {
  19.   CppEvent<ISubject, int> event;
  20.  
  21. public:
  22.   inline virtual void Attach(EventHandler<IListener, ISubject, int>* handler)
  23.   {
  24.     event.attach(handler);
  25.   }
  26.  
  27.   inline virtual void Detach(EventHandler<IListener, ISubject, int>* handler)
  28.   {
  29.     event.detach(handler);
  30.   }
  31.  
  32.   void submit()
  33.   {
  34.     event.notify(this, 2);
  35.   }
  36. };
  37.  
  38. class Listener: public IListener
  39. {
  40.   EventHandler<IListener, ISubject, int>* _handler;
  41.   ISubject * _subject;
  42. public:
  43.   Listener(ISubject *s)
  44.   {
  45.     _subject = s;
  46.     _handler = new EventHandler<IListener, ISubject, int>(this, &IListener::updated);
  47.     s->Attach(_handler);
  48.   }
  49.   virtual void updated(ISubject* subject, int i)
  50.   {
  51.     std::cout << i <<std::endl;
  52.     std::cin.get();
  53.   }
  54.   ~Listener()
  55.   {
  56.     _subject->Detach(_handler);
  57.     delete _handler;
  58.   }
  59. };
  60.  
  61. int _tmain(int argc, _TCHAR* argv[])
  62. {
  63.   Subject* s = new Subject();
  64.   Listener* l = new Listener(s);
  65.   
  66.  
  67.   s->submit();
  68.  
  69.   delete l;
  70.   delete s;
  71.   return 0;
  72. }
* This source code was highlighted with Source Code Highlighter.
Получилось в итоге что-то похожее на события в .NET.
Мне, как разработчику в основном использующему C#, кажется, что не очень хорошо, когда при подписке нужно обязательно указывать экземпляр подписчика. Но что делать, от этого пока никуда не деться. Компромиссное решение - выделить интерфейсы получателя и подписчика, что и сделано в примере.

Заключение

Наш же пример показывает, что удобно, когда
1. Обработчики не имеют возвращаемых значений.
2. Через событие передается и источник события.
Кстати, получившийся класс события рассчитан только на один параметр. Я считаю, что этого вполне достаточно, чтобы передавать любую информацию из источника. Так, например, и сделано в .NET.

На самом деле новый стандарт C++11 предоставляет более гибкие возможности для реализации событий. Например, лямбда-выражения и std::function могут существенно облегчит жизнь разработчику. Но это уже другая история.

4 комментария:

  1. Для реализации Наблюдателя удобно использовать signals или signals2 (поддержка многопоточности) из boost. Или (если boost использовать нельзя) создать свою реализацию на их основе. В boost обработчики могут иметь возвращаемые значения, поддерживаются приоритеты, параметров обработчика может быть несколько, возможность автоматического отсоединения сигнала от слота при удалении объекта, обработчики не обязаны быть методами класса и т.д.
    Лямда-выражения это просто функторы создаваемые "на лету", а function обеспечивает вызов только одного "обобщенного" делегата т.е. при реализации событий они представляют меньший интерес, чем signals.
    Пример 1 будет работать криво (например: _observers.assign(1, obs) - обозреватель всегда будет 1; зачем здесь _count? и т.д.). В последнем примере то же: m_handlers.assign(1, handler). Последний пример не безопасен относительно исключений: надо использовать RAII. Проблемы также возникнут, если Subject будет удален раньше, чем Listener и т.д.
    Таким образом, ИМХО, предложеная реализация требует доработки для использования в реальных приложениях или для того, чтобы стать хорошим примером.

    ОтветитьУдалить
    Ответы
    1. Я же сказал, что удобно пользоваться уже готовыми решениями. См. первый абзац. Ну а если делать события свои, то не обязательно делать это на основе boost. Все зависит от поставленной задачи. Я показал, как добиться какой-то базовой функциональности. Если кому-нибудь нужно что-то сверх, бери и делай.
      По поводу примера 1. Почему обозреватель будет 1? А count это да - забыл убрать.
      Буду признателен, если ты приведешь код, как в моем примере использовать RAII. Про исключения, это ты верно заметил.
      Тем не менее, я считаю этот пример хорошим. Он верно передает суть многих реализаций событий.

      Удалить
    2. В приведенном коде, кстати, еще много "дыр". Просто нет времени досконально все проверять. Например, "интерфейс" ISubject/IListener не годен для серьезного использования (поскольку он error prone) т.к. не имеет виртуального деструктора. С++ это не С#: здесь интерфейсов нет, есть только абстрактные классы.

      Удалить
    3. "По поводу примера 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.

      Удалить