Комментарии 22
А где объяснения про этот странный метод 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()),...);
ps: А вот к реализации IsPressed много вопросов
1. где фильтрация дребезга?
2. почему название функции не соответствует происходящему в ней?
3. почему опрос кнопок идёт с неконтролируемой скоростью?
4. как определяются начальные состояния лампочек?
- Это же для примера, давайте предположим, что в данном случае это решено аппаратно.
- Не понял вопроса, кнопка нажата — возвращаем true. Нажатие определяется по надавил, отпустил.
- Потому что это пример… для упрощения. Идет бесконечный опрос кнопки, как только кнопка нажата (определяется по фазе Надавил-Отпустил), надо оповестить подписчиков.
- Никак, он просто переключается. Это опять же пример простой.
Суть была, в том, чтобы показать использование шаблона, а не алгоритм обработки нажатия на кнопку, зачем лишний код нести. Определение отпускания сделал только для того, чтобы постоянно не определялось нажатие кнопки.
И другой вопрос почему не происходит девиртуализация в втором варианте? А если собирать lto или поставить final?
Там в исходнике по ссылке всё в одном файле, тест был бы бессмысленным — компилятор бы всё упростил до одного и того же кода во всех трёх случаях. Поэтому я предполагаю, все тесты без оптимизации.
Ага, тогда у вас по сути девиртуализация через crtp только variadic crtp получился?
Если что — я не автор, я только предположил про оптимизацию, на проект глядючи — у автора в конце есть ссылка. :) Признаться, я не вижу тут CRTP, но в целом да, я бы сказал что только в последнем случае будет девиртуализация. Точнее, даже виртуализации не будет, потому что передача через шаблонные аргументы, и после разворота всего этого у компилятора уже будут конкретные типы и, наверное, не будет ни интерфейса, ни таблицы виртуальных функций.
Тесты действительно без оптимизации.
Но, я сделал проверку на максимальной оптимизации — таблицы остаются и девиртуализации не происходит, что в первом, что во втором варианте.
3 вариант ужимается до 88 байт (по сути оставил проверку порта на 0 (для кнопки) и просто три раза поменять состояние портов для 3 светодиодов), остальные ужимаются не сильно...
Статическая подписка с использованием шаблона Наблюдатель на примере С++ и микроконтроллера Cortex M4