Всем доброго здравия!
В преддверии Нового года хочу продолжить рассказывать про использование С++ на микроконтроллерах, на этот раз попытаюсь рассказать про использование шаблона Наблюдатель (но далее я буду называть его Издатель-Подписчик или просто Подписчик, такой вот каламбур), а также реализацию статической подписки на С++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)...) ;
}
...
} ;
В методе 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 лежит тут
Всех с наступающим! И удачи в новом году!
technic93
А где объяснения про этот странный метод pass(), зачем он нужен?
lamerok Автор
Добавил с статью:
В методе
Notify()
есть вызов функцииpass()
, она используется для того, чтобы развернуть параметры шаблона с переменным количеством аргументовРеализация функции
pass()
проста до невообразимости, это просто функция, принимающая переменное количество аргументов:Как же происходит разворачивание в несколько вызовов функции
HandleEvent()
для каждого из подписчиков.Поскольку функция
pass()
принимает несколько аргументов любого типа, то в нее можно передать несколько аргументов типаbool
, например, можно вызвать функциюpass(true, true, true)
. При этом конечно ничего не произойдет, но нам и не нужно.Строка
(subscribers.HandleEvent() , true)
использует оператор "," (запятая), который выполняет оба операнда (слева направо) и возвращает значение второго оператора, т.е здесь вначале выполнитсяsubscribers.HandleEvent()
, затемtrue
и в функциюpass()
будет подставленоtrue
.Ну а "..." это стандартная запись для разворачивания переменного количества аргументов. Для нашего случая, очень схематично действия компилятора можно описать следующим образом:
technic93
Спасибо за подробности, но почему нельзя было просто
subscribers.HandleEvent()...
или сделать опять же initializer_list и обойти через for? Я очень редко использую variadic templates поэтому не знаю нюансов. Наверное первый мой вариант не скомпилируется а вот накладывает ли какие то дополнительные расходы второй я не знаю.lamerok Автор
Просто сделать
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 можно передавать только объекты одного типа. Что-то типа этого, могу ошибиться, не компилил...
Ну и плюсом сама переменная
subscribersList
типаinitializer_list
и её обход через цикл, тоже ресурсы отъедает. Он конечно при оптимизации скорее всего свернется, но без оптимизации там точно будут накладные расходы.А так, никаких накладных, если еще сделать все принудительно inline, вообще ровно в 3 строчки развернется...
nimonic
В принципе от
pass()
можно избавиться:Скорее всего, компилятор исключит само действие с числами.
Эх, жаль что в MSVS 2017 этим не воспользоваться: "fatal error C1001: An internal error has occurred in the compiler."
lamerok Автор
Согласен можно через fold expression, там только думаю компилятор ворнинг может дать, что результат выражения нигде использоваться не будет. А с предупреждением жить не очень хорошо.
nimonic
да, предупреждение будет. Можно сделать так
char tmp = ((subscribers.HandleEvent(),0)+...);
Интересно, это как-то повлияет на размер в ПЗУ?
lamerok Автор
Попробую проверить дома, сейчас под рукой нет компилятора...
esaulenka
Не знаю, как у IAR'а, а в GCC это не влияет никак:
https://godbolt.org/z/KDhRfo
правда, появляется другой варнинг — про неиспользуемый tmp.
можно приделать ещё один костыль вида
но понятнее код от этого не становится...
nimonic
Раз уж мы используем С++17, то лучше использовать [[maybe_unused]]
Это хорошо, что никакой разницы, это правильно. Поэтому лучше без
pass
— кода меньше, а по поводу читаемости — в любом случае свёртка выглядит как минимум не привычно.vamireh
Зачем вообще все эти приседания, если можно просто
lamerok Автор
Точно :}, оператор "," поддерживается для fold expression. Застрял в С++14 с этим passом. Добавлю в статью.
kovserg
Он просто, что бы вызвать все HandleEvent по очереди.
ps: А вот к реализации IsPressed много вопросов
1. где фильтрация дребезга?
2. почему название функции не соответствует происходящему в ней?
3. почему опрос кнопок идёт с неконтролируемой скоростью?
4. как определяются начальные состояния лампочек?
lamerok Автор
Суть была, в том, чтобы показать использование шаблона, а не алгоритм обработки нажатия на кнопку, зачем лишний код нести. Определение отпускания сделал только для того, чтобы постоянно не определялось нажатие кнопки.
technic93
И другой вопрос почему не происходит девиртуализация в втором варианте? А если собирать lto или поставить final?
Falstaff
Там в исходнике по ссылке всё в одном файле, тест был бы бессмысленным — компилятор бы всё упростил до одного и того же кода во всех трёх случаях. Поэтому я предполагаю, все тесты без оптимизации.
technic93
Ага, тогда у вас по сути девиртуализация через crtp только variadic crtp получился?
Falstaff
Если что — я не автор, я только предположил про оптимизацию, на проект глядючи — у автора в конце есть ссылка. :) Признаться, я не вижу тут CRTP, но в целом да, я бы сказал что только в последнем случае будет девиртуализация. Точнее, даже виртуализации не будет, потому что передача через шаблонные аргументы, и после разворота всего этого у компилятора уже будут конкретные типы и, наверное, не будет ни интерфейса, ни таблицы виртуальных функций.
technic93
После этого ответа я подумал что опять не заметил что перевод, но нет :)
borisxm
Там вообще никакая девиртуализация ненужна. В таком «шаблонном» подходе класс делают композитным, без виртуальных методов. При компиляции получаем псевдоразворот циклов, что при небольшом числе подписчиков оптимальней чем классический цикл с виртуальными методами, т.к. компилятор видит все типы и реализации. Даже если реализация вынесена из заголовков, LTO все равно успешно делает инлайны.
lamerok Автор
Тесты действительно без оптимизации.
Но, я сделал проверку на максимальной оптимизации — таблицы остаются и девиртуализации не происходит, что в первом, что во втором варианте.
3 вариант ужимается до 88 байт (по сути оставил проверку порта на 0 (для кнопки) и просто три раза поменять состояние портов для 3 светодиодов), остальные ужимаются не сильно...
FD4A
Спасибо за статью, сечайс засяду изучать её вниметельнее. Это мой любимый шаблон, постоянно применяю в своём сишном коде в виде структуры, которая содержит массив указателей на функции инициализируемый в main(). На работе почти всё взаимодействие в системе между процессами по этому шаблону.