Всем доброго здравия!


В преддверии Нового года хочу продолжить рассказывать про использование С++ на микроконтроллерах, на этот раз попытаюсь рассказать про использование шаблона Наблюдатель (но далее я буду называть его Издатель-Подписчик или просто Подписчик, такой вот каламбур), а также реализацию статической подписки на С++17 и преимущества этого подхода в некоторых приложениях.


Введение


Шаблон Подписчик один из самых распространенных шаблонов, которые используются в разработке ПО. С его помощью, например, делают обработку нажатия кнопок в Windows Form. Да и вообще в любом месте где нужно отреагировать как-то на изменения параметров системы, будь то изменения в файлах или обновление измеренного значения от датчика самое время не думая использовать шаблон Подписчик.


Преимущество шаблона заключается в том, что мы развязываем знания об Издателе и Подписчике, не привязываясь к конкретным объектам. Можем подписать кого угодно к кому угодно, при этом не затрагивая реализацию объектов Издателя и Подписчика.


Начальные условия


Перед тем как начнем знакомиться с шаблоном, давайте вначале договоримся, что мы хотим разрабатывать надежное ПО, в котором:


  • не используем динамического выделения памяти
  • по минимуму сводим работу с указателями
  • используем как можно больше констант, чтобы никто никого по возможности не мог менять
  • но при этом используем как можно меньше констант расположенных в ОЗУ

А теперь давайте рассмотрим стандартную реализацию шаблона Подписчик.


Стандартная реализация


Предположим у нас есть кнопка и вот при нажатии на кнопку нам надо моргнуть светодиодами, но сколько их будет пока неизвестно, да и вообще, моргать возможно нужно не светодиодами, а прожектором на корабле для передачи сообщений азбукой Морзе. Важно, что мы не знаем кто будет подписываться. К сожалению, у меня нет прожектора под рукой, поэтому все примеры в статья ради простоты и лучшего понимания сделаны со светодиодами.


Итак, при нажатии на кнопку, необходимо оповестить светодиод об этом нажатии. В свою очередь, узнав о нажатии светодиод должен переключиться в противоположное состояние.
Стандартная реализация на языке UML выглядит следующим образом...



Здесь ButtonController класс отвечающий за опрос кнопки и оповещение подписчиков о нажатии, а Led в данном случае подписчик. Эти два класса развязаны между собой посредством интерфейсов IPublisher и ISubsriber и ни один из классов не знает про другой. Таким образом, любой объект наследующий интерфейс ISubscriber может подписаться на событие от ButtonController.


Поскольку динамическое выделение памяти запрещено, то я объявил массив из 3 элементов для подписки. Т.е. максимум может быть 3 подписчика. Вот так в первом приближении может выглядеть метод оповещения подписчиков у класса ButttonsController


struct ButtonController : IPublisher 
{  
  void Run() 
  {
    for(;;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const override
  {
    // Пробегаемся по списку подписчиков и вызываем у них метод HandleEvent()
    for(auto it: pSubscribers)
    {
      if (it != nullptr)
      {
        it->HandleEvent() ;
      }
    }
  }
} ;

Вся соль находится в методе Notify() класса Publisher. В этом методе мы пробегаемся по списку подписчиков и вызываем у каждого из них метод HandleEvent() и это круто, потому что каждый подписчик реализует этот метод по своему и может делать там все что душе угодно (на самом деле тут надо быть осторожным, а то черт его знает, что там делает подписчик, вы же можете вызвать его метод, например, и из прерывания и надо быть бдительным, чтобы не позволять подписчикам делать долгие и плохие вещи)


В нашем случае, светодиоду позволено делать все что угодно, поэтому он делает переключение своего состояния:


template <typename Port, std::uint32_t pinNum>
struct Led: ISubscriber                          
{
  static void Toggle()
  {
    Port::ODR::Toggle(1 << pinNum);
  }

  void HandleEvent() override
  {
    //Собственно это то, ради чего все затевалось, моргнуть
    Toggle() ; 
  }
};

Полная реализация всех классов

template<typename Port, std::size_t pinNum>
struct Button
{
  static bool IsPressed()
  {
    bool result = false;
    if ((Port::IDR::Read() & (1 << pinNum)) == 0) //если кнопка нажата
    {
      while ((Port::IDR::Read() & (1 << pinNum)) == 0) // ждем пока не отожмут
      {
      };
      result = true;
    }
    return result;
  }
} ;

// Пользовательская кнопка на порте GPIOC.13
using UserButton = Button<GPIOC, 13> ;

struct ISubscriber
{
  virtual void HandleEvent() = 0;
} ;

struct IPublisher
{
  virtual void Notify() const = 0;
  virtual void Subscribe(ISubscriber* subscriber) = 0;
} ;

template <typename Port, std::uint32_t pinNum>
struct Led: ISubscriber                          
{

  static void Toggle()
  {
    Port::ODR::Toggle(1 << pinNum);
  }

  void HandleEvent() override
  {
    Toggle() ;
  }
};

struct ButtonController : IPublisher
{  
  void Run() 
  {
    for(; ;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const override
  {
    for(auto it: pSubscribers)
    {
      if (it != nullptr)
      {
        it->HandleEvent() ;
      }
    }
  }

  void Subscribe(ISubscriber* subscriber) override
  {
    if (index < pSubscribers.size()) 
    {
      pSubscribers[index] = subscriber ;
      index ++ ;
    }
   // Если больше 3 подписчиков то курить...чисто для примера
  }

private:  
  std::array<ISubscriber*, 3> pSubscribers ;
  std::size_t index = 0U ;
} ;

А как подписка может выглядеть в коде? А вот так:



int main()
{
  // Светодиод Led1 подключен к выводу 5 порта GPIOC
  static Led<GPIOC,5> Led1 ;  
  // Светодиод Led2 подключен к выводу 8 порта GPIOC
  static Led<GPIOC,8> Led2 ;
  // Светодиод Led3 подключен к выводу 9 порта GPIOC
  static Led<GPIOC,9> Led3 ;

  ButtonController buttonController ;

  // Подписываем 3 светодиода
  buttonController.Subscribe(&Led1) ;
  buttonController.Subscribe(&Led2) ;
  buttonController.Subscribe(&Led3) ;

  // Запускаем контроллер на вечный опрос кнопки
  buttonController.Run() ;
}

Хорошая новость заключается здесь в том, что мы можем подписать любой объект и время его создания нам неважно. Это может быть глобальный объект, статический или локальный. С одной стороны это хорошо, а с другой зачем в данном коде нам делать подписку в runtime. Ведь по сути здесь адрес объектов Led1, Led2, Led3 известен на этапе компиляции. Так почему нельзя подписаться еще на этапе компиляции и держать массив указателей на подписчиков в ПЗУ?


Кроме того, здесь есть риск потенциальных ошибок, например, многие ли задумывались, что произойдет при вызове метода Subsсribe(), если он будет вызваться из нескольких потоков? Мы ограничены всего 3 подписчиками, а что будет, если мы подпишем 4 светодиод?


В большинстве случаев нам эта подписка нужна один раз в жизни при инициализации, мы просто сохраняем указатели на подписчиков и все. Указатель будет всю жизнь хранить адрес этих подписчиков. И неминуем тот день, когда он может быть испорчен из-за вспышки сверхновой (конечно, если рассматривать довольно длительный интервал времени). Но в любом случае вероятность отказа ОЗУ намного выше чем ПЗУ и хранить постоянные данные в ОЗУ не рекомендуется.


Ну и совсем плохая новость, такое архитектурное решение занимает оооооочень много места и в ПЗУ и в ОЗУ. На всякий случай запишем, сколько ПЗУ и ОЗУ занимает это решение:


Module ro code ro data rw data
main.o 488 64 21

Т.е. в сумме 552 байта в ПЗУ и 21 байт в ОЗУ — скажем так не очень для того, чтобы нажать на кнопку и моргнуть тремя светодидами.


Ну и чтобы обезопасить себя от таких вот неприятностей и уменьшить потребление ресурсов контроллера давайте рассмотрим вариант со статической подпиской.


Статическая подписка


Для того, чтобы сделать подписку статической можно использовать несколько подходов. Назову их так:


  • Традиционный — тот же самый подход, но с использованием constexpr конструктора и заданием списка подписчиков через него.
  • Нетрадиционный С использованием шаблонов — передать список подписчиков через параметры шаблона. (здесь шаблон — это определение из области метапрограммирования, а не шаблонов проектирования)

Традиционный подход к статической подписке


Попробуем сделать подписку на этапе компиляции. Для этого немного подправим нашу архитектуру:



Картинка мало чем отличается от изначальной, но есть несколько различий: удален метод Subscribe() и теперь подписка будет осуществляться непосредственно в конструкторе. Конструктор должен принимать переменное число аргументов, а для того, чтобы можно было подписаться статически на этапе компиляции он будет constexpr. В нем будет инициализироваться массив подписчиков и эта инициализация может быть проведена во время компиляции:


struct ButtonController : IPublisher
{
  template<typename... Args>
  constexpr ButtonController(Args const*... args): pSubscribers()
  {
    std::initializer_list<ISubscriber const*> result = {args...} ;
    std::size_t index = 0U;

    for(auto it: result)
    {
      if (index < size)
      {
        pSubscribers[index] = const_cast<ISubscriber*>(it);
      }
      index ++ ;
    }      
  }

private:  
  static constexpr std::size_t size = 3U;
  ISubscriber* pSubscribers[size] ;  
} ;

Полный код для такой реализации
struct ISubscriber
{
  virtual void HandleEvent() const  = 0;
} ;

struct IPublisher
{
  virtual void Notify() const = 0;
} ;

template<typename Port, std::size_t pinNum>
struct Button
{
  static bool IsPressed()
  {
    bool result = false;
    if ((Port::IDR::Read() & (1 << pinNum)) == 0) //если кнопка нажата
    {
      while ((Port::IDR::Read() & (1 << pinNum)) == 0) // ждем пока не отожмут
      {
      };
      result = true;
    }
    return result;
  }
} ;

template <typename Port, std::uint32_t pinNum>
struct Led: ISubscriber                          
{
  constexpr Led()
  {
  }

  static void Toggle()
  {
    Port::ODR::Toggle(1<<pinNum);
  }

  void HandleEvent() const override
  {
    Toggle() ;
  }
};

// Пользовательская кнопка на порте GPIOC.13
using UserButton = Button<GPIOC, 13> ;

struct ButtonController : IPublisher
{
  template<typename... Args>
  constexpr ButtonController(Args const*... args): pSubscribers()
  {
    std::initializer_list<ISubscriber const*> result = {args...} ;
    std::size_t index = 0U;

    for(auto it: result)
    {
      if (index < size)
      {
        pSubscribers[index] = const_cast<ISubscriber*>(it);
      }
      index ++ ;
    }      
  }

  void Run() const
  {
    for(; ;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const override
  {
    for(auto it: pSubscribers)
    {
      if (it != nullptr)
      {
        it->HandleEvent() ;
      }
    }
  }

private:  
  static constexpr std::size_t size = 3U;
  ISubscriber* pSubscribers[size] ;  
} ;

Теперь подписку можно сделать во время компиляции:


int main()
{
   // Светодиод Led1 подключен к выводу 5 порта GPIOC
   static constexpr Led<GPIOC,5> Led1 ;  
   // Светодиод Led2 подключен к выводу 8 порта GPIOC
   static constexpr Led<GPIOC,8> Led2 ;
   // Светодиод Led3 подключен к выводу 9 порта GPIOC
   static constexpr Led<GPIOC,9> Led3 ;

   static constexpr ButtonController buttonController(&Led1, &Led2, &Led3) ;  

   buttonController.Run() ;

   return 0 ;
} ;

Здесь объект buttonController полностью расположился в ПЗУ вместе с массивом указателей на подписчиков:


main::buttonController 0x800'1f04 0x10 Data main.o [1]

Все вроде бы ничего, за исключением того, что мы опять ограничены всего 3 подписчиками. А еще класс издателя должен иметь constexpr конструктор и вообще быть полностью константным, чтобы гарантированно положить указатель на подписчиков в ПЗУ, иначе даже при известных адресах подписчиков наш объект вместе со всем содержим опять отправится в ОЗУ.


Из других минусов — так как по прежнему используются виртуальные функции, то таблицы виртуальных функций понемногу отгрызают нашу ПЗУ. А ресурс это хоть и доступный, но не бесконечный. В большинстве применений, на него можно забить и взять микроконтроллер побольше, но часто бывает так, что каждый байт на счету, особенно если речь идет о продуктах выпускаемых сотнями тысяч, как например, датчики измерения физических велечин.


Посмотрим, как обстоят дела с памятью в этом решении:


Module ro code ro data rw data
main.o 172 76 0

И хотя здесь результат "ошеломляющий": общее потребление ОЗУ — 0 байт, а ПЗУ 248 байт, что в два раза меньше, чем в первом решении, чувствуется, что есть еще потенциал для улучшений. Из этих 248 байт примерно 50 как раз занимают таблицы виртуальных методов.


Небольшое отступление:
Шаг в размере ПЗУ 256 кБайт у современных микроконтроллеров это норма, (например Cortex M4 микроконтроллер фирмы TI имеет 256 кБайт ПЗУ, а следующий вариант уже с 512 кБайт). И будет не очень хорошо, когда из-за 50 лишних байт нам придется брать контроллер с ПЗУ на 256 кБайт большего размера и дороже, поэтому отказавшись от виртуальных функций можно сэкономить… целых 50 центов (разница между микроконтроллером в 256 и 512 кБайт ПЗУ составляет около 50-60 центов).


Это звучит смешно для 1 микроконтроллера, но на партии в 400 000 устройств в год, можно сэкономить 200 000 долларов. Уже не так смешно, а учитывая, что за такое рац. предложение могут наградить грамотой и подарочной картой на 3000 рублей, совсем не остается сомнений в правильности отказа от виртуальных функций и экономии лишних 50 байтов в ПЗУ.


Нетрадиционный подход


Давайте посмотрим, как можно сделать тоже самое без виртуальных функций и сэкономить еще немного ПЗУ.


Вначале прикинем как это может быть:


int main()
{
   // Светодиод Led1 подключен к выводу 5 порта GPIOC
   static Led<GPIOC,5> Led1 ;  
   // Светодиод Led2 подключен к выводу 8 порта GPIOC
   static Led<GPIOC,8> Led2 ;
   // Светодиод Led3 подключен к выводу 9 порта GPIOC
   static Led<GPIOC,9> Led3 ;
   //Светодиоды подписываются на 
   ButtonController<Led1, Led2, Led3> buttonController ;  

   buttonController.Run() ;  

  return 0 ;
}

Наша задача развязать два объекта Издатель(ButtonController) и Подписчик(Led) друг от друга, чтобы они знать про друг друга не знали, но при этом ButtonController мог оповестить Led.


Можно объявить класс ButtonController каким-то таким образом.


template <Led<GPIOC,5>& subscriber1, 
          Led<GPIOC,8>& subscriber2, 
          Led<GPIOC,9>& subscriber3>
struct ButtonController
{ 
  void Run() const
    {
      for(; ;)
      {
        if (UserButton::IsPressed())
        {
          Notify() ;
        }
      }
    }

    void Notify() const
    {
      subscriber1.HandleEvent() ;
      subscriber2.HandleEvent() ;
      subscriber3.HandleEvent() ;
    }
...
} ;

Но сами понимаете, здесь мы привязываемся к конкретным типам и нам придется каждый раз в новом проекте переделывать определение класса BbuttonController. А хотелось бы в новом проекте просто взять и использовать ButtonController без заморочек.


На помощь приходит С++17, где можно не указывать тип, а попросить компилятор вывести тип за вас — это как раз то, что надо. Мы можем точно также, как и в традиционном подходе развязать знания об Издателе и Подписчике, при этом количество подписчиков практически не ограничено.


template <auto& ... subscribers>
struct ButtonController
{ 
  void Run() const
  {
    for(; ;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const
  {
    pass((subscribers.HandleEvent() , true)...) ;
  }
...
} ;

Как работает функция pass(..)

В методе Notify() есть вызов функции pass(), она используется для того, чтобы развернуть параметры шаблона с переменным количеством аргументов


 void Notify() const
  {
    pass((subscribers.HandleEvent() , true)...) ;
  }

Реализация функции pass() проста до невообразимости, это просто функция, принимающая переменное количество аргументов:


template<typename... Args>
  void pass(Args...)  const   { }
} ;

Как же происходит разворачивание в несколько вызовов функции HandleEvent() для каждого из подписчиков.


Поскольку функция pass() принимает несколько аргументов любого типа, то в нее можно передать несколько аргументов типа bool, например, можно вызвать функцию pass(true, true, true). При этом конечно ничего не произойдет, но нам и не нужно.


Строка (subscribers.HandleEvent() , true) использует оператор "," (запятая), который выполняет оба операнда (слева направо) и возвращает значение второго оператора, т.е здесь вначале выполнится subscribers.HandleEvent(), затем true и в функцию pass() будет подставлено true.


Ну а "..." это стандартная запись для разворачивания переменного количества аргументов. Для нашего случая, очень схематично действия компилятора можно описать следующим образом:


pass((subscribers.HandleEvent() , true)...) ; ->

pass((Led1.HandleEvent() , true), 
    (Led2.HandleEvent() , true), 
    (Led3.HandleEvent() , true)) ; -> 

Led1.HandleEvent() ; ->
pass(true,  
    (Led2.HandleEvent() , true), 
    (Led3.HandleEvent() , true)) ; -> 

Led2.HandleEvent() ; ->
pass(true,  
     true, 
    (Led3.HandleEvent() , true)) ; -> 

Led3.HandleEvent() ; ->
pass(true,  
     true, 
     true) ; 

Вместо ссылок можно использовать указатели:


template <auto* ... subscribers>
struct ButtonController
{ 
...
} ;

Дополнение: На самом деле, спасибо vamireh, который указал на то, что все эти танцы с бубном pass функцией в С++17 не нужны. Так как оператор "," запятая поддерживается в fold expression (которые были введены в стандарт С++ 17), то код упрощается еще:


template <auto& ... subscribers>
struct ButtonController
{ 
  void Run() const
  {
    for(; ;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const
  {
    ((subscribers.HandleEvent()), ...) ;
  }
} ;

Архитектурно это выглядит вообще очень просто:



Я тут добавил еще LCD класс, но чисто для примера, чтобы показать, что теперь без разницы на тип и количество подписчиков, главное чтобы у него бы реализован метод HandleEvent().


Да и весь код в общем-то тоже теперь проще:


template<typename Port, std::size_t pinNum>
struct Button
{
  static bool IsPressed()
  {
    bool result = false;
    if ((Port::IDR::Read() & (1 << pinNum)) == 0) //если кнопка нажата
    {
      while ((Port::IDR::Read() & (1 << pinNum)) == 0) // ждем пока не отожмут
      {
      };
      result = true;
    }
    return result;
  }
} ;

// Пользовательская кнопка на порте GPIOC.13
using UserButton = Button<GPIOC, 13> ;

template <typename Port, std::uint32_t pinNum>
struct Led                          
{

  static void Toggle()
  {
    Port::ODR::Toggle(1<<pinNum);
  }

  void HandleEvent() const
  {
    Toggle() ;
  }
};

template <auto& ... subscribers>
struct ButtonController
{
  void Run() const
  {
    for(; ;)
    {
      if (UserButton::IsPressed())
      {
        Notify() ;
      }
    }
  }

  void Notify() const
  {
    ((subscribers.HandleEvent()), ...) ;
  }  
} ;

int main()
{
   // Светодиод Led1 подключен к выводу 5 порта GPIOC
   static constexpr Led<GPIOC,5> Led1 ;  
   // Светодиод Led2 подключен к выводу 8 порта GPIOC
   static constexpr Led<GPIOC,8> Led2 ;
   // Светодиод Led3 подключен к выводу 9 порта GPIOC
   static constexpr Led<GPIOC,9> Led3 ;
   static constexpr ButtonController<Led1, Led2, Led3> buttonController ;  

   buttonController.Run() ;  
   return 0 ;
}

Вызов Notify() в методе Run() вырождается в простой последовательный вызов


Led1.HandleEvent() ; 
Led2.HandleEvent() ;
Led3.HandleEvent() ;

Как же обстоят дела с памятью здесь?


Module ro code ro data rw data
main.o 186 4 0

ПЗУ всего 190 байт и 0 байт ОЗУ. Вот теперь порядок, это почти в 3 раза меньше по размеру чем стандартный вариант, при этом выполняет он ровно тоже самое.


Таким образом, если у вас в приложении заранее известны адреса подписчиков и вы следуете условиям, определенным в начале статьи


Условия в начале статьи
  • не используем динамического выделения памяти
  • по минимуму сводим работу с указателями
  • используем как можно больше констант, чтобы никто никого по возможности не мог менять
  • но при этом используем как можно меньше констант расположенных в ОЗУ

С уверенностью можно использовать такую вот реализацию шаблона Издатель-Подписчик для уменьшения строк кода и экономии ресурсов, а там глядишь и можно претендовать не только на подарочную карту, но и премию по результатам года.


Тестовый пример под IAR 8.40.2 лежит тут


Всех с наступающим! И удачи в новом году!

Комментарии (22)


  1. technic93
    25.12.2019 01:17

    А где объяснения про этот странный метод pass(), зачем он нужен?


    1. lamerok Автор
      25.12.2019 09:13

      Добавил с статью:


      Как работает функция pass(..)

      В методе Notify() есть вызов функции pass(), она используется для того, чтобы развернуть параметры шаблона с переменным количеством аргументов


       void Notify() const
        {
          pass((subscribers.HandleEvent() , true)...) ;
        }

      Реализация функции pass() проста до невообразимости, это просто функция, принимающая переменное количество аргументов:


      template<typename... Args>
        void pass(Args...)  const   { }
      } ;

      Как же происходит разворачивание в несколько вызовов функции HandleEvent() для каждого из подписчиков.


      Поскольку функция pass() принимает несколько аргументов любого типа, то в нее можно передать несколько аргументов типа bool, например, можно вызвать функцию pass(true, true, true). При этом конечно ничего не произойдет, но нам и не нужно.


      Строка (subscribers.HandleEvent() , true) использует оператор "," (запятая), который выполняет оба операнда (слева направо) и возвращает значение второго оператора, т.е здесь вначале выполнится subscribers.HandleEvent(), затем true и в функцию pass() будет подставлено true.


      Ну а "..." это стандартная запись для разворачивания переменного количества аргументов. Для нашего случая, очень схематично действия компилятора можно описать следующим образом:


      pass((subscribers.HandleEvent() , true)...) ; ->
      
      pass((Led1.HandleEvent() , true), 
          (Led2.HandleEvent() , true), 
          (Led3.HandleEvent() , true)) ; -> 
      
      Led1.HandleEvent() ; ->
      pass(true,  
          (Led2.HandleEvent() , true), 
          (Led3.HandleEvent() , true)) ; -> 
      
      Led2.HandleEvent() ; ->
      pass(true,  
           true, 
          (Led3.HandleEvent() , true)) ; -> 
      
      Led3.HandleEvent() ; ->
      pass(true,  
           true, 
           true) ; 


      1. technic93
        25.12.2019 10:26
        +1

        Спасибо за подробности, но почему нельзя было просто subscribers.HandleEvent()... или сделать опять же initializer_list и обойти через for? Я очень редко использую variadic templates поэтому не знаю нюансов. Наверное первый мой вариант не скомпилируется а вот накладывает ли какие то дополнительные расходы второй я не знаю.


        1. lamerok Автор
          25.12.2019 11:53

          Просто сделать subscribers.HandleEvent()... нельзя. Потому что использовать переменное количество параметров можно только через Function argument lists, Parenthesized initializers, Brace-enclosed initializers, Template argument lists, Function parameter list, Template parameter list, Base specifiers and member initializer lists, Lambda captures, Fold-expressions, Using-declarations, Dynamic exception specifications, The sizeof… operator.
          Более подробно здесь можно прочитать.


          И просто subscribers.HandleEvent()... ни под один из этих вариантов не подходит.


          Через initializer_list можно, но придется вводить общий интерфейс для подписчиков, так как типы подписчиков разные, а в initializer_list можно передавать только объекты одного типа. Что-то типа этого, могу ошибиться, не компилил...


              auto subscribersList  = {(ISubscriber *)(&subscribers)...} ;
          
              for(auto subsriber: subscribersList)
              {
                  subsriber->HandleEvent() ;
              }      

          Ну и плюсом сама переменная subscribersList типа initializer_list и её обход через цикл, тоже ресурсы отъедает. Он конечно при оптимизации скорее всего свернется, но без оптимизации там точно будут накладные расходы.


          А так, никаких накладных, если еще сделать все принудительно inline, вообще ровно в 3 строчки развернется...


      1. nimonic
        25.12.2019 10:56

        В принципе от pass() можно избавиться:


        void Notify() const { ((subscribers.HandleEvent(), 0) + ...); }

        Скорее всего, компилятор исключит само действие с числами.
        Эх, жаль что в MSVS 2017 этим не воспользоваться: "fatal error C1001: An internal error has occurred in the compiler."


        1. lamerok Автор
          25.12.2019 10:58

          Согласен можно через fold expression, там только думаю компилятор ворнинг может дать, что результат выражения нигде использоваться не будет. А с предупреждением жить не очень хорошо.


          1. nimonic
            25.12.2019 11:13

            да, предупреждение будет. Можно сделать так char tmp = ((subscribers.HandleEvent(),0)+...);
            Интересно, это как-то повлияет на размер в ПЗУ?


            1. lamerok Автор
              25.12.2019 11:57

              Попробую проверить дома, сейчас под рукой нет компилятора...


            1. esaulenka
              25.12.2019 15:17

              Не знаю, как у IAR'а, а в GCC это не влияет никак:
              https://godbolt.org/z/KDhRfo
              правда, появляется другой варнинг — про неиспользуемый tmp.


              можно приделать ещё один костыль вида


                  (void) ((subscribers.HandleEvent(), 0) + ...) ;

              но понятнее код от этого не становится...


              1. nimonic
                25.12.2019 16:27
                +1

                Раз уж мы используем С++17, то лучше использовать [[maybe_unused]]


                    [[maybe_unused]] auto tmp = ((subscribers.HandleEvent(), 0) + ...) ;

                Это хорошо, что никакой разницы, это правильно. Поэтому лучше без pass — кода меньше, а по поводу читаемости — в любом случае свёртка выглядит как минимум не привычно.


              1. vamireh
                25.12.2019 16:39
                +1

                Зачем вообще все эти приседания, если можно просто

                ((subscribers.HandleEvent()),...);


                1. lamerok Автор
                  25.12.2019 16:42

                  Точно :}, оператор "," поддерживается для fold expression. Застрял в С++14 с этим passом. Добавлю в статью.


    1. kovserg
      25.12.2019 09:18

      Он просто, что бы вызвать все HandleEvent по очереди.

      ps: А вот к реализации IsPressed много вопросов
      1. где фильтрация дребезга?
      2. почему название функции не соответствует происходящему в ней?
      3. почему опрос кнопок идёт с неконтролируемой скоростью?
      4. как определяются начальные состояния лампочек?


      1. lamerok Автор
        25.12.2019 09:28

        1. Это же для примера, давайте предположим, что в данном случае это решено аппаратно.
        2. Не понял вопроса, кнопка нажата — возвращаем true. Нажатие определяется по надавил, отпустил.
        3. Потому что это пример… для упрощения. Идет бесконечный опрос кнопки, как только кнопка нажата (определяется по фазе Надавил-Отпустил), надо оповестить подписчиков.
        4. Никак, он просто переключается. Это опять же пример простой.

        Суть была, в том, чтобы показать использование шаблона, а не алгоритм обработки нажатия на кнопку, зачем лишний код нести. Определение отпускания сделал только для того, чтобы постоянно не определялось нажатие кнопки.


  1. technic93
    25.12.2019 01:20

    И другой вопрос почему не происходит девиртуализация в втором варианте? А если собирать lto или поставить final?


    1. Falstaff
      25.12.2019 01:23

      Там в исходнике по ссылке всё в одном файле, тест был бы бессмысленным — компилятор бы всё упростил до одного и того же кода во всех трёх случаях. Поэтому я предполагаю, все тесты без оптимизации.


      1. technic93
        25.12.2019 01:43

        Ага, тогда у вас по сути девиртуализация через crtp только variadic crtp получился?


        1. Falstaff
          25.12.2019 02:20

          Если что — я не автор, я только предположил про оптимизацию, на проект глядючи — у автора в конце есть ссылка. :) Признаться, я не вижу тут CRTP, но в целом да, я бы сказал что только в последнем случае будет девиртуализация. Точнее, даже виртуализации не будет, потому что передача через шаблонные аргументы, и после разворота всего этого у компилятора уже будут конкретные типы и, наверное, не будет ни интерфейса, ни таблицы виртуальных функций.


          1. technic93
            25.12.2019 02:35

            После этого ответа я подумал что опять не заметил что перевод, но нет :)


        1. borisxm
          25.12.2019 11:29

          Там вообще никакая девиртуализация ненужна. В таком «шаблонном» подходе класс делают композитным, без виртуальных методов. При компиляции получаем псевдоразворот циклов, что при небольшом числе подписчиков оптимальней чем классический цикл с виртуальными методами, т.к. компилятор видит все типы и реализации. Даже если реализация вынесена из заголовков, LTO все равно успешно делает инлайны.


      1. lamerok Автор
        25.12.2019 09:39

        Тесты действительно без оптимизации.


        Но, я сделал проверку на максимальной оптимизации — таблицы остаются и девиртуализации не происходит, что в первом, что во втором варианте.


        3 вариант ужимается до 88 байт (по сути оставил проверку порта на 0 (для кнопки) и просто три раза поменять состояние портов для 3 светодиодов), остальные ужимаются не сильно...


  1. FD4A
    25.12.2019 07:45

    Спасибо за статью, сечайс засяду изучать её вниметельнее. Это мой любимый шаблон, постоянно применяю в своём сишном коде в виде структуры, которая содержит массив указателей на функции инициализируемый в main(). На работе почти всё взаимодействие в системе между процессами по этому шаблону.