Как стать автором
Обновить

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

Время на прочтение12 мин
Количество просмотров8.7K
Всего голосов 18: ↑18 и ↓0+18
Комментарии22

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

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

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


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

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

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

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


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

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

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

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

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

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


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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

Публикации