Статическая подписка с использованием шаблона Наблюдатель на примере С++ и микроконтроллера Cortex M4


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


    В преддверии Нового года хочу продолжить рассказывать про использование С++ на микроконтроллерах, на этот раз попытаюсь рассказать про использование шаблона Наблюдатель (но далее я буду называть его Издатель-Подписчик или просто Подписчик, такой вот каламбур), а также реализацию статической подписки на С++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 лежит тут


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

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 22

      +1

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

        0

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


        Как работает функция 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

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

            0

            Просто сделать 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 строчки развернется...

            0

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


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

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

              0

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

                0

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

                  0

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

                    0

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


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


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

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

                      +1

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


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

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

                        +1
                        Зачем вообще все эти приседания, если можно просто
                        ((subscribers.HandleEvent()),...);

                          0

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

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

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

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

                0

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

                  0

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

                    0

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

                      +1

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

                        0

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

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

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


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


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

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

                      Only users with full accounts can post comments. Log in, please.