C++ Embedded
Открыть в Telegram
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Больше446
Подписчики
Нет данных24 часа
+27 дней
+430 день
Архив постов
446
nlohmann_json
Пусть FreeRTOS немного передохнет, пока мы кидаем камни в огород одной известной библиотеки для перекладывания json.
Намедни подрядился я в ближайшей it-деревне json-ы пасти. Дело нехитрое, с утра выгоняешь их на парсинг из локальных сокетов, которые местные демоны держат. Доишь из них данные и отправляешь пастись в сети. Инструмент, говорят, бери какой хочешь, но у нас есть только nlohmann_json.
Ладно, история сердцу знакомая. Вроде как современная библиотека для новейших плюсов, такое нам по нраву.
Достаем первый json, примерно такого содержания:
auto text = R"({ "pi": 3.141, "happy": true })";
Парсим в точности, как написано в инструкции:
nlohmann::json ex1 = nlohmann::json::parse(text);
Проверяем результат, просто бросая полученный объект в стандартный вывод:
std::cout << ex1 <<std::endl; // {"happy":true,"pi":3.141}
Отлично работает. Здесь меня обуял жуткий приступ перфекционизма: зря, что ли, AUTOSAR придумали? Надо сделать как положено! Решительно переписываю:
nlohmann::json const ex1 {nlohmann::json::parse(text)};
И... вся дальнейшая обработка ломается с треском вдребезги пополам. Стандартный вывод показывает:
[{"happy":true,"pi":3.141}]
Хм... ранее распознанный элемент неожиданно вообразил себя массивом?
За ответами лезем под капот библиотеки, и узнаем, что json - это на самом деле basic_json<>;
Далее, к ужасу своему, обнаруживаем std::initializer_list в одном из конструкторов!
basic_json(initializer_list_t init,
bool type_deduction = true,
value_t manual_type = value_t::array) { ... }
using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
Этот лист - коварный фрукт. Сами можете посмотреть в раздел стандарта 12.2.2.8 Initialization by list-initialization [over.match.list].
Если конструктор инициализируется списком параметров, либо конструктор по умолчанию отсутствует, сначала выполняется такое разрешение перегрузки, где функциями-кандидатами являются конструкторы инициализируемые списком (9.4.5).То есть если объект создается через braced-init-list, значением в фигурных скобочках
T{v}, а класс не агрегатный, то предпочтение всегда будет отдаваться конструктору с std::initializer_list.
Например:
struct MegaJson {
MegaJson() = default;
MegaJson(int) {}
MegaJson(std::initializer_list<int>) {}
};
Если мы напишем MegaJson j1 {1};
то при наличии std::initializer_list во втором конструкторе, на первый даже смотреть не станут.
Если мы не используем list-initialization MegaJson j1 (1);
то в дело пойдет первый конструктор.
В json-библиотеке дела обстоят еще веселее, чем в консерватории. Здесь std::initializer_list чугунной жопой заслоняет аж целый конструктор копий, поскольку в списке используются ссылки на сам класс basic_json. Для наглядности вернемся к упрощенной структуре MegaJson:
struct MegaJson {
MegaJson() = default;
MegaJson(MegaJson const &) {}
MegaJson(std::initializer_list<std::reference_wrapper<MegaJson>>) {}
};
MegaJson j1 {};
MegaJson j2 {j1};
вместо копирующего конструктора будет вызван другой. Все по стандарту, не подкопаешься.
Тогда скрипя зубами и скрепя сердце, я написал
auto const ex2 = nlohmann::json::parse(text);
Однако описанное выше поведение контринтуитивно, и вообще это безобразие. Нильсу следовало бы использовать здесь variadic templates и не выпендриваться.446
Run, std::thread, run!
Третий день плачу, чернил не достать, как, впрочем, и приличной четырехслойной бумаги. Эта зима научила нас, что сквотировать дачные домики в низкий сезон не лучшее решение, как и получение воды путем термической обработки снега. Понять это несложно, достаточно посмотреть, как превращается сейчас грохочущая слякоть в потоки не очень чистой жидкости. Совсем как таски во FreeRTOS под давлением нашего болезненного разума обращаются в самые настоящие
std::thread! Настало время испытаний.
Предположим, мы создали обычный проект с поддержкой C++ через STM32CubeIDE для платы NUCLEO-L452RE. Другие девборды у меня все равно отобрали при увольнении. Серия L4 вообще заточена под сверхнизкое энегропотребление и не очень богата ресурсами, особенно мало оперативной памяти. Гейтс когда-то говорил, что 640 КБ должно хватить всем, а мне приходится довольствоваться 160 КБ. Однако если заработает здесь, то на более жирных платах уж точно.
Так вот, негодный Куб создает main.c файл, придется переименовать его вручную в main.cpp, чтоб собрался с плюсами как надо.
Немного изменим сгенерированную уныло-стандартную функцию main и ее окрестности:
struct Data { ... };
int main() {
HAL_Init();
...
Data data { ... };
std::thread main_thread {StartDefaultTask, &data};
...
osKernelStart();
std::abort();
}
Во-первых, создадим структуру Data, данные из которой понадобятся нам в потоке StartDefaultTask. Во-вторых, уберем стандартное начало для запуска потоков системой
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128); defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);Уж больно тянет от этого кода замшелой сишечкой. Заменим на вариант поинтереснее:
std::thread main_thread {StartDefaultTask, &data};
Хорошо, но здесь поток не запустится. А чего вы хотели? Шедулер еще не работает.
Стартанет он при вызове osKernelStart, и больше мы в функцию main не вернемся. Планировщик распределяет время между тасками, а main пролетит мимо тазика. Поэтому следующая за вызовом планировщика функция std::abort никогда не должна быть вызвана. Но пусть останется, на случай, если что-то пойдет не так.
Жмем на запуск отладки и сильно удивляемся, что все пошло в разнос: данные в структуре data превратились в кашу!
А все потому, что при запуске планировщика система вызывает такую функцию:
static void prvPortStartFirstTask( void ) {
__asm volatile(
" ldr r0, =0xE000ED08 \n" /* NVIC offset register */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n" /* Set the msp back to the start of the stack. */
...
" nop \n");
}
Вкратце, здесь мы загружаем адрес из регистра по адресу 0xE000ED08, а это Vector Table Offset Register. Загружаем первый адрес из таблицы векторов прерываний. Позвольте напомнить, что первым там идет указатель на начало стека. Инструкция msr возвращает в регистр msp адрес начала стека. Это значит, что все локальные переменные, созданные на стеке функции main, под угрозой. Да что там, им всем каюк! Более того, технически мы не покидаем main, поэтому деструкторы локальных объектов никогда не будут вызваны.
Щекотливая ситуация. Можно чуть исправить саму функцию prvPortStartFirstTask, добавить инструкцию sub, т.е. сдвинуть стек системы, оставив функции main маленькую щелочку для ее объектов: sub r0, r0, #0x100
Либо, что более предпочтительно для FreeRTOS, вообще не создавать локальных переменных в main. Перенесем все логику в MainThread и просто запустим этот поток через статический объект.
void MainThread() { ... }
static std::thread main_thread {MainThread};
Из этого потока мы не собираемся возвращаться, поэтому даже join не нужен. Это будет нашей новой точкой входа.446
std::thread & FreeRTOS
Нихао всем, кто еще не выкинул елку. Верной дорогой идете, товарищи! Сейчас же еще китайский новый год праздновать. В этот день принято шуметь, чтоб отогнать злых духов. Мы же сделаем им больно иным путем: используем таски FreeRTOS для создания потоков а-ля С++. Поскольку GCC собран без поддержки многопоточности, никаких стандартных потоков у нас нет, про FreeRTOS компилятор тоже не имеет никакого представления. Если все же подключить заголовочный файл
<thread> и создать нечто такое: std::thread first{}; то все соберется, вот только объект first будет абсолютно бесполезен. Никакого потока мы не запустим. Заглянув внутрь, сразу увидим где рылся крымский хан:
class thread {
public:
#ifdef _GLIBCXX_HAS_GTHREADS
...
Все самое интересное внутри класса вырезано дефайном _GLIBCXX_HAS_GTHREADS, который не определен, если компилятор беспоточный. В принципе логично, но можно ли одурачить компилятор и насильно определить этот дефайн?
Добавим -D_GLIBCXX_HAS_GTHREADS в Tool settings проекта и... ужаснемся количеству ошибок при сборке.
Стандартную библиотеку не пересоберешь, а вновь открывшиеся участки кода требуют сатисфакции: функции работы с мьютексами, условными переменными и т.п. оказались без определения. Похоже, придется переписать их все для FreeRTOS. Но... мы и так собирались это делать. Пока просто застабим все, на что компилятор грязно ругается.
Нас интересуют не шпили, но функции без реализации из thread.cc вроде thread::join() или thread::detach().
В первую очередь нам нужно аккуратно реализовать _M_start_thread! Для этого нам нужно определить тип thread::id::native_handle_type.
Поскольку теперь native_handle_type просто псевдоним __gthread_t, то напишем:
typedef struct gthread {
TaskHandle_t taskID;
friend bool operator==(const gthread& a, const gthread& b);
constexpr std::strong_ordering operator<=>(const gthread& a) const;
} __gthread_t;
Имя типа TaskHandle_t как бы подсказывает нам, что переменная taskID играет роль потокового дескриптора в этой системе, поэтому нам нужно его сохранить. Еще нам потребуются функции сравнения потоков, дабы знать как их различать, но эти задачи тривиальны.
Реализуем _M_start_thread через вызов функции xTaskCreate, которая создает новую таску во FreeRTOS.
void thread::_M_start_thread(thread::_State_ptr state, void (*)()) {
BaseType_t result = xTaskCreate(static_cast<TaskFunction_t>(&execute_native_thread_routine),
(const portCHAR *)"c++thread", // имя таски
128, // глубина стека в словах (не байтах!)
state.get(), // параметр для вызываемой функции
tskIDLE_PRIORITY + 1, // приоритет такси
&this->_M_id._M_thread); // дексриптор потока
if (result != pdPASS) { std::abort(); }
state.release();
}
Элементарно! Здесь создаем таску FreeRTOS с именем c++thread, глубиной стека в 128 слов (мне для демонстрационных целей хватит), приоритетом чуть выше, чем Idle. Тут же мы воспользуемся приемом, что видели и в обычном запуске тредов. Передаем указатель unique_ptr как void*, а я что сделаю?! xTaskCreate принимает только функции типа TaskFunction_t, а у них в качестве аргумента только указатель на void.
Ах, да! Функция execute_native_thread_routine:
static void execute_native_thread_routine(void* __p) {
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
vTaskDelete(NULL);
}
Здесь мы работаем с умным указателем и запускаем целевую функции через _M_run. Однако если функция завершится, нам нужно корректно удалить текущую таску, поэтому вызываем vTaskDelete(NULL) в конце. Синь нянь куай лэ, тащемта!446
thread::_M_start_thread
"... они много кричат об опасности усреднения в художественном переводе, но позвольте... - небритый теоретик с воскового цвета лицом картинно вскинул руку, - ... другая сторона медали, где художественный перевод есть оглупление автора и читателя..."
"Отец, ты не из иняза? - лекция мне надоела, - а то заточка твоя уж больно знакомая". Аккуратно плечом оттер увлекшегося оратора от контейнера. Много вкусного и полезного может найти там вынужденный фриган, если хорошенько пороется.
А если хорошенько покопаться в исходниках GCC, можно найти даже готовую к употреблению функцию
_M_start_thread в файле thread.cc:
void thread::_M_start_thread(_State_ptr state, void (*depend)()) {
...
const int err = __gthread_create(&_M_id._M_thread, &execute_native_thread_routine, state.get());
if (err) __throw_system_error(err);
state.release();
Для тех, кто забыл, т.е. для меня, напомним, что в качестве аргументов в функцию передается умный указатель state и какой-то малопонятный depend. Второй аргумент вообще не несет никакой алгоритмической нагрузки, его пока отложим.
Вот внутреннее значение умного указателя нам еще пригодится, мы его отдадим в вызов функции __gthread_create. Также туда мы сплавим и указатель на _M_id._M_thread. Вы, конечно, помните, что у класса thread есть член _M_id типа id, содержащий в себе _M_thread - душу потока. Естественно, туда же передадим указатель на функцию execute_native_thread_routine, непосредственно она и будет запущена в отдельном потоке.
Ой, какая стыдная картина нам тут открывается, просто грязный хак: в функцию мы передаем сырой указатель, полученный через state.get().
В конце функции наш unique_ptr не уничтожается, но обнуляется вызовом release(). Что поделаешь, __gthread_create пришел из незамутненного сишного мира, где нет этих ваших срам-поинтеров.
В функции __gthread_create нет абсолютно ничего примечательного, кроме того, реализация зависит от платформы
static inline int __gthread_create (__gthread_t *__threadid, void *(*__func) (void*), void *__args) {
return __gthrw_(pthread_create) (__threadid, NULL, __func, __args);
}
Чаще всего это просто системный вызов pthread_create. Думаю, все знают какие аргументы он ждет.
И вот стартует новый поток, execute_native_thread_routine начинает исполнять.
static void* execute_native_thread_routine(void* __p) {
thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
__t->_M_run();
return nullptr;
}
Здесь все предельно просто. Воссоздаем уникальный указатель thread::_State_ptr, ну в самом деле, не в вручную же его удалять? Запускаем наконец функцию, которую мы изначально и хотели запустить в потоке. Вот и дождались, легким движением руки вызов _M_run превращается в вызов целевой функции со всеми ее сохраненными аргументами. Если мы не ставили себе целью запустить бесконечный поток, а лишь использовать его временно, то рано или поздно из функции мы выйдем, _M_run завершится функция вернет nullptr, умный указатель освободит сохраненную функцию с ее многострадальными аргументами. И все!
Какой второй аргумент? А, depend! Ну, он тут просто так, участвует только в одной операции внутри функции:
asm ("" : : "rm" (depend));
Штука это не новая, нечто подобное использовал для реализации своего std::launder. Здесь так же, ассемблерная вставка нужна для предотвращения оптимизации, даже LTO побрезгует оптимизировать такой кошмар. Обычно в качестве второго аргумента _M_start_thread выступает функция _M_thread_deps_never_run.
static void _M_thread_deps_never_run() {
reinterpret_cast<void (*)(void)>(&pthread_create)();
reinterpret_cast<void (*)(void)>(&pthread_join)();
}
Очередной грязынй трюк. Чтоб не полагаться на вызов функций gthread, мы засветим нужные функции заранее в аргументе, который гарантированно не будет оптимизирован. Сильные ссылки на функции pthread_* точно будут.
Теперь, когда все с этими потоками стало ясно, самое время реализовать их для FreeRTOS. A suivre.446
std::thread
Очень волнительно вдруг стать безработным. Длительное пребывание в таком статусе уже не особо беспокоит. Все как-то меньше желания выделывать коленца на собеседованиях перед пресыщенной публикой. Лучше поспать, или погулять возле свалки, или... продолжить дьявольские изыскания по натягиванию плюсов на FreeRTOS! Помнится, с
thread_local мы более-менее разобрались. Только зачем нам оно, если у нас нет нормальных потоков? Конечно, на микроконтроллерах это никому не нужно, но можем же?!
Давайте посмотрим, достаточно ли глубока кроличья нора, чтоб сунуть туда еще и std::thread.
Взглянем, наконец, на std::thread с точки зрения не заклинателя потоков, а разработчика. Несмотря на кажущуюся сложность, структура std::thread проста: он содержит только один член класса _M_id типа id. Класс id внутренний с неким хэндлером _M_thread, типа native_handle_type.
class id {
native_handle_type _M_thread;
public:
id() noexcept : _M_thread() { }
explicit id(native_handle_type __id) : _M_thread(__id) { }
...
Что скрывается под этой маской, зависит от конкретной реализации, но именно _M_thread держит в себе внутреннюю суть потока.
Создавая объект потока, мы ожидаем мгновенного запуска.
std::thread first {func, arg1, arg2};
Т.е. вызов конструктора first тут же породит поток, в котором будет вызвана функция func с аргументами arg1 и arg2.
Закрадывается подозрение, что конструктор потока лишь удобная обертка для вызова системных функций.
Вот как выглядит конструктор std::thread, если убрать малозначительные детали:
template<typename _Callable, typename... _Args, typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable&& __f, _Args&&... __args) {
static_assert(...); // std::thread arguments must be invocable
using _Wrapper = _Call_wrapper<_Callable, _Args...>;
_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
nullptr);
}
Хорошо видно, что в конструктор можно пихать все, что угодно. К сожалению, конструктор ограничивает лишь SFINAE, который запрещает передавать внутрь объекты thread. Иначе это будет уже конструктором копирования. Хорошо бы еще сразу на входе отсечь невызываемые объекты, чтоб не добавлять головной боли метапрограммистам. Иначе они опять решат, что выражение std::thread{1} имеет смысл.
Однако ограничение есть только внутри конструктора в виде static_assert, что не очень хорошая практика:
static_assert( __is_invocable<typename decay<_Callable>::type, typename decay<_Args>::type...>::value)
Ведь из-за этого следующее выражение очень даже пройдет валидацию:
static_assert(std::is_constructible_v<std::thread, int>); // Сомнительно, но ОК
А потом сборка упадет в корчах, измарав весь вывод малопонятными сообщениями об ошибках.
Наконец, спустимся еще ниже и найдем там вызов функции _M_start_thread. Именно она выполняет всю грязную работу.
К ее реализации еще вернемся, а сейчас нас интересует что же за красота такая в аргументах.
Там создает объект типа _State_ptr, он же std::unique_ptr<_State>.
Сразу понятно, что _State - базовый класс:
struct _State {
virtual ~_State();
virtual void _M_run() = 0;
};
Конструируемый наследник _State_impl содержит в себе некий функционалный объект, который может быть исполнен путем вызова _M_run.
Этот объект вызывается без аргументов, и если мы хотим в потоке вызвать функцию с аргументами, то надо еще постараться. Хорошо, что делать это за нас будет _Wrapper.
Он же _Call_wrapper, он же _Invoker, создает обертку вызова желаемой функции с некоторым количеством аргументов. Т.е. просто впитает в себя func, arg1, arg2 и будет хранить в tuple, пока не настанет час Ч. И где-то внутри _M_start_thread вызовет func(arg1, arg2), прием давно известный и малоинтересный. Хорошо, что это уже реализовано до нас.
Фуф, после праздников нельзя так резко начинать. Надо поспать или погулять... _M_start_thread никуда не убежит. A suivre.446
never trust a voise on the phone
Звонок. "Вас приветствует компания "Машинно-тракторная станция"! К сожалению, заканчивается срок действия вашего договора..." - стал зачитывать скрипт противный гнусавый голос.
Всегда отношусь с подозрением к звонкам с неизвестного номера, но здесь жир буквально сочился через трубку. Во-первых, эта схема развода уже сто лет как известна. Во-вторых, от красной компании я в ужасе бежал еще три тысячи лет назад, прихватив на прощание номер. Об этом не знают только всякие жухалы, что и выдает их с головой. Не говоря уже о предложениях, по степени нелепости сравнимых только с просьбой пускать ветра на зажигалку в общественном месте.
"Договор просто необходимо продлить, - с жаром подтверждаю я, подавив первоначальный импульс немедля закончить разговор, - непременнейшие!"
"Хорошо, сейчас придет смс..."
"Да, да, да!.. - почти кричу я, - уже пришла, там написано, цитирую: катись колбаской по Малой Спасской! Ага, и код есть: 53"
"Почему 53?" - неуверенно осведомился гнусавый.
"Да потому, что ты отброс!"
Это шутка для своих. По-японски число 53 можно прочитать как "го-ми", что в переводе значит просто "мусор".
Дальше я не стал давить языковыми познаниями, а просто выдал псевдоспециалисту службы поддержки опсоса весь вокабуляр, бережно собранный во времена работы грузчиком на атомной станции. Гнусавого не хватило надолго, и он, заплакав, повесил трубку.
Получи! На мякине не проведешь! Обычно вообще звонки не принимаю с незнакомых и подозрительных номеров, но сейчас я обновил резюме и надеялся, что позвонит какой-нибудь Цукербрин.
Вечером того же дня мои ожидания оправдались.
"Здравствуйте! Я из компании "Большая Медведица", - голос сразу показался мне приятным, хоть и был с нарушением резонаторной функции носовой полости, - видели ваше резюме, вы и правда собрались увольняться?"
"Да, думаю об этом".
"В общем, не буду ходить вокруг да около, нам все нравится, даже собеседование нет нужды проводить. Однако есть маленькая проблема"
"Какая?" - внутреннее ликование сменилось тревогой.
"Вы же работаете в компании "Малая Медведица", а это наш подрядчик. Мы не можем вас переманивать и даже обсуждать наше предложение..."
У нас и правда были проекты с этим заказчиком.
"Я понял, чтоб вы сделали мне предложение, мне нужно быть свободным, как ветер?"
"Вы очень догадливы. Дайте нам знать, когда напишите заявление. Не затягивайте."
На этом наш разговор закончился. После столь воодушевляющего звонка я, обдумав предложение секунд пять, написал менеджеру, что устал. Потом написал коллегам, что ухожу. Собственноручно состряпал заявление о непреодолимом желании покинуть стройные ряды компании "Малая Медведица".
Положенные две последующие недели я провел как на иголках. Таинственному благодетелю из "Большой Медведицы" о принятом решении я сообщил сразу. В ответ получил короткое: "Хорошо". Больше им не писал, чтоб не думали, будто они мне нужны больше, чем я им. Еще свои условия будут диктовать. Мол, мне податься больше некуда, других предложений нет.
Так прошла неделя. Я был тверд, как скала. Они тоже не писали. "Наверное ждут, пока уволюсь окончательно...", - не отчаивался я.
Прошло две недели. Я получил расчет, у меня отобрали всю скопившуюся коллекцию плат для разработки. Делать стало решительно нечего. На связь никто не выходил.
На исходе третьей недели, когда костлявая рука голода протянула свои холодные пальцы к моему горлу, твердость моя дала слабину и стала походить на весеннюю снежную жижу. Махнув рукой на гордость я написал сообщение, где бесстыдно умолял сделать мне оффер, они ведь хотели, хоть прямо и не говорили.
В ответ пришел лишь подмигивающий смайлик и пара цифр: 5 и 3.
446
FreeRTOS & __emutls_get_address
Безработным быть не так уж и плохо. Не нужно больше вставать чуть свет, можно поваляться до шести утра. Не спеша встать, приготовить горький кофе, поплакать немного под песню "Я хочу быть кочегаром" и продолжить свои дьявольские эксперименты по интенсивному скрещиванию плюсов с FreeRTOS. Удивительно, но в эту ОСВР завезли аналогичную
thread_local функциональность. Без поддержки в языке, конечно, но вызовом пары свободных функций можно добиться желаемого эффекта.
void vTaskSetThreadLocalStoragePointer(TaskHandle_t xTaskToSet, BaseType_t xIndex, void *pvValue)
Устанавливает некий указатель pvValue по некоему глобальному индексу xIndex для некоего потока (или задачи) xTaskToSet. Если же xTaskToSet равен NULL, то переданный указатель будет сохранен для текущего потока. Можно воспринимать это как двухмерный массив указателей, где индексами служат значения xIndex и дескриптор потока.
Чтоб получить это значение, нам понадобится функция:
void *pvTaskGetThreadLocalStoragePointer(TaskHandle_t xTaskToQuery, BaseType_t xIndex)
Тут ожидаемо фигурирует глобальный индекс xIndex. Опять же xTaskToQuery можно не указывать, тогда вернется актуальное значение указателя для текущего потока.
На самом деле каждый поток резервирует массив указателей в себе, размер массива определяется дефайном configNUM_THREAD_LOCAL_STORAGE_POINTERS.
С такими функциями не нужно ничего выдумывать, стоит просто начать реализовывать.
Попробуем переопределить многократно упомянутую ранее функцию поиска. Просто вписать void* __emutls_get_address(__emutls_object*) не получится:
error: definition of 'void* __emutls_get_address(__emutls_object*)' ambiguates built-in declaration 'void* __emutls_get_address(void*)'Придется использовать такую сигнатуру, как у встроенной функции, ибо кое-кто не хочет светить свои внутренние структуры наружу. Понимаем.
void *__emutls_get_address (void *o);
уже внутри без помех приведем указатель к нужному типу:
struct __emutls_object * obj = static_cast<struct __emutls_object *>(o);
Получается, что каждый новый объект управления должен получить свой уникальный номер, индекс в массиве. Используем сквозную нумерацию, введем счетчик:
static int __emutls_index {};
будем увеличивать его каждый раз, когда функция поиска вызывается для нового управляющего объекта. Хранить полученный ассоциированный индекс можно хоть в переменной obj->loc.offset. Условимся, что нулевое значение указывает на отсутствие назначенного индекса.
if (obj->loc.offset == 0) {
obj->loc.offset = ++__emutls_index;
}
Далее попробуем получить наш указатель через pvTaskGetThreadLocalStoragePointer(NULL, index).
Если фиаско мы потерпели, значит, память под целевой объект еще даже не выделена. Тут же исправим ситуацию, выделив выровненную память (через emutls_alloc, например) и сохранив указатель на нее в vTaskSetThreadLocalStoragePointer.
И все, и все, и все... сохранять в управляющем объекте этот указатель не имеет смысла, просто вернем его.
Итого, самая простая реализация thread_local для FreeRTOS будет выглядеть так:
extern "C" void *__emutls_get_address (void *o) {
struct __emutls_object * obj = static_cast<struct __emutls_object *>(o);
if (obj->loc.offset == 0) {
obj->loc.offset = ++__emutls_index;
}
int const index {static_cast<int32_t>(obj->loc.offset) - 1};
void *ptr = pvTaskGetThreadLocalStoragePointer(NULL, index);
if (ptr == nullptr) {
ptr = emutls_alloc(obj);
vTaskSetThreadLocalStoragePointer(NULL, index, ptr);
}
return ptr;
}
Довольно примитивно, зато останется еще время нарезать новогодний салат. Хотя после увольнения со временем у меня теперь проблем нет. Еще нормализовалось давление, появился здоровый румянец на лице и иных местах. Лохматость повысилась... того и гляди, почки придут в нормативное состояние, дороже стоить будут.
Тем ихтиандрам, кто не выдержал и отписался, традиционно, попутного ветра и успешных собесов в ООО "Геенна Огненная".
Остальных, сильным духом и крепких умом, поздравляю с наступающими праздниками! С Новым кодом!446
single-thread & __emutls_get_address
"Мечта у меня имеется. Хочу под FreeRTOS на плюсах писать. Чтоб там
std::thread были и всякое..." - неосторожно вырвалось у меня во время аттестации и вместо повышения грейда вышло увольнение. Однако это не должно нас останавливать!
Итак, мы решили подменить функцию поиска __emutls_get_address. Входным аргументом функции будет указатель на объект управления.
Что это за объект? Обратимся за разъяснениями к GCC. В его исходном коде можно найти некую подозрительную структуру __emutls_object.
Выглядит она следующим образом:
struct __emutls_object {
word size;
word align;
union {
pointer offset;
void *ptr;
} loc;
void *templ;
};
Интуитивно понятно, что такое word и pointer:
typedef unsigned int word __attribute__((mode(word)));
typedef unsigned int pointer __attribute__((mode(pointer)));
Тип целочисленный, но вот ширина определяется через атрибут mode. Режим word определяет ширину однословного целого числа, а pointer задаст такую же ширину, как и у указателя.
Теперь рассмотрим, из чего состоит управляющий объект:
size - это, конечно, размер целевого класса. Столько памяти мы должны выделить под объект в текущем потоке.
align - величина выравнивания целевого класса.
В функцию поиска мы ворвемся с уже установленными параметрами. Остальные члены структуры вообще непонятно для чего, вернемся к ним позже.
Когда же вызывается функция поиска? Вспомним наш пример:
int safe_func(int t) {
thread_local Info common {};
common.x += t;
...
Специально для safe_func компилятор создаст в где-то памяти объект типа __emutls_object, и когда мы заходим обратиться к common, он "найдет" нужный экземпляр объекта через __emutls_get_address, передав ей указатель на управляющий объект. Функция вернет указатель на найденный целевой объект (актуальный для текущего потока).
Сомневаюсь, что здесь собрались любители мучить велосипеды, поэтому одним глазком взглянем, как GCC реализует функцию поиска.
Сначала рассмотрим вариант для компилятора, собранного без поддержки потоков, все основные события развернутся в первой части функции:
void * __emutls_get_address (struct __emutls_object *obj) {
if (! __gthread_active_p ()) {
if (obj->loc.ptr == NULL)) obj->loc.ptr = emutls_alloc (obj);
return obj->loc.ptr;
}
...
Здесь функция __gthread_active_p проверяет на всякий случай, активна ли система потоков, если да, то возвращает 1, иначе 0.
Поскольку потоки не поддерживаются, то мы попадаем внутрь if и проверяем, выделена ли уже память для целевого объекта. Если нет, то запрашиваем память через emutls_alloc.
Потоков нет, поэтому одного участка памяти достаточно, его можно записать в obj->loc.ptr и впоследствии всегда использовать этот указатель.
Почему нужна специальная функция для выделения памяти? Почему не malloc? Как вы помните, управляющий объект содержит не только размер, но и величину выравнивания. Т.е. память нужно выделить уже выровненную, чего malloc делать не умеет.
Еще один момент, emutls_alloc инициализирует участок памяти. Выглядит это так:
if (obj->templ) memcpy (ret, obj->templ, obj->size); else memset (ret, 0, obj->size);Вот нам и пригодился последний неопознанный параметр
obj->templ. Если он указывает на какой-то паттерн, то им целевой объект и инициализируется.
Предельно простое поведение при однопоточной модели компилятора. Теперь понятно, почему thread_local переменные абсолютно "сломаны" во FreeRTOS.
Зато мы видим, как это можно исправить, думаю, управимся до праздников. A suivre.446
thread_local
Я просыпаюсь в холодном поту. Резко вскинувшись от ужаса, остаюсь сидеть в кровати. Снова снился FreeRTOS. Вырвавшиеся на свободу таски с остервенением рвали незащищенные структуры данных. "Почему ты не защитил нас!.." - доносился до меня их предсмертный хрип. Я пометил их как
thread_local, но это не работало. Безжалостные и неумолимые жернова многопоточности растерзали и перемололи хрупкие участки памяти. Почему же такая защита не помогла?
Думаю, все знают, что такое thread_local. Переменная с такой меткой в каждом потоке имеет свое собственное значение. Яркой иллюстрацией может послужить переменная errno. Устанавливая ее в отдельном потоке, мы не хотим, чтоб ее значение было видно в другом.
Стандарт опять же, в разделе 6.7.5.3 Thread storage duration [basic.stc.thread], гарантирует нам, что:
Переменные, объявленные с ключевым словом thread_local, хранятся, пока существует специальное потоковое вместилище. Мамой клянусь, оно будет существовать, пока выполняется поток. Каждому потоку достанется отдельный объект или ссылка на него. Объявленное имя будет связано с сущностью текущего потока.Из этого воспоследует, что неплохо бы озаботиться неким специальным участком памяти для хранения такого типа переменных. Он есть, и имя ему TLS (thread local storage). Как это будет реализовано, отдается на откуп компилятору. Есть специальная опция для сборки GCC, которая выбирает метод доступа к локальному хранилищу потока
-mtls-dialect=gnu. Посмотрим, какой же метод выбран у нас. Наш-то arm-none-eabi-g++ собран с --disable-tls, т.е. целевая платформа вообще не поддерживает TLS. Логично, у нас тут настоящий embedded, а не гулькин клюв. Эксплуатируемый нами GCC жесткий, собран без поддержки потоков. Зачем TLS?
Да, что-то вроде потоков есть во FreeRTOS, но без поддержки GCC thread_local ничего не стоит. Конечно, компилятор не посмеет отказать нам в использовании ключевого слова.
Код соберется, GCC не будет грязно ругаться. Дело в том, что даже собранный с --disable-tls компилятор хоть чучелком, хоть тушкой, но обязан поддержать спецификатор. В этом случае управление TLS переходит с т.н. emulated TLS, то есть эмуляции потоковых хранилищ.
Прослойка эмуляции создает объект управления для целевого объекта. Для доступа к целевому объекту TLS имеется функция поиска, которая при указании адреса объекта управления вернет адрес экземпляра целевого объекта для текущего потока.
Рассмотрим, как это работает на практике. Для пущего веселья определим некую структуру Info:
struct Info {
int x;
int y;
};
Теперь напишем якобы thread-safe функцию, выполняющую бессмысленные действие:
int safe_func(int t) {
thread_local Info common {};
common.x += t;
common.y += t;
return common.x + common.y;
}
Сначала может показаться, что common - это локальная переменная. Гоните от себя эту мысль! Стандарт недвусмысленно намекает в разделе 9.2.2 Storage class specifiers [dcl.stc]:
Если переменная помечена как thread_local, то она по умолчанию интерпретируется как static, если иной спецификатор не объявлен явно.На математическую составляющую можно смело закрыть глаза, нас интересует только доступ к целевому объекту - переменной
common. Вот как это выглядит в ассемблерном кружеве:
080005f0 <safe_func(int)>:
80005f0: push {r7, lr}
80005f2: sub sp, #8
80005f4: add r7, sp, #0
80005f6: str r0, [r7, #4]
80005f8: ldr r0, [pc, #36]
80005fa: bl 800053c <__emutls_get_address>
80005fe: mov r3, r0
Доступ к объекту выдает функция __emutls_get_address, это и есть упомянутая выше функция поиска.
Сигнатура функции: void *__emutls_get_address (struct __emutls_object *obj), где obj - указатель на управляющий объект.
И тут нас всех осеняет: "А можно ли ее подменить?" A suivre.446
std::is_bounded_array or std::is_unbounded_array
Легко писать про безобразные собеседования. Чем хуже оно проходит, тем лучше. Совсем превосходно, если интервью заканчивается грязной дракой. О, сколько контента можно навалить опосля! Поэтому складывается впечатление, что индустрия на грани, специалистов не хватает, и все очень-очень плохо. Однако это не совсем так, и приятные исключения все же бывают. Вот третьего дня пришли ко мне люди из молодой и подающей надежды компании А-стор, схватили и понесли на собеседование.
Там суровый мужчина не задавал странных вопросов. Все они так или иначе касались предметной области.
"Вот есть такая функция lseek... может ли он шагнуть за границу файла?" - интервьюер с надеждой посмотрел на меня.
"Дааа...", - говорю, а сам пытаюсь вспомнить хоть что-то про эту функцию, - "границы вещь такая..."
Вот Уильям Пенн Эдер "Уилл" Роджерс говорил, что когда невежество начинается, то границ оно не знает. Кстати, это эпиграф к предложению "Traits for [Un]bounded Arrays" от 2019 года. Автор предлагал ввести в язык новые метафункции:
is_bounded_array и is_unbounded_array.
Это очень просто, ведь массив можно определить как T[N] - то есть внутри скобок указан размер N, это bounded array.
Либо задать массив как T[], это unbounded array.
Думаю, опытные разработчики только фыркнут, мол, такое сделать несложно:
template<typename T>
inline constexpr bool is_bounded_array_v = false;
template<typename T, size_t Size>
inline constexpr bool is_bounded_array_v<T[Size]> = true;
Если тип T совпадает с шаблоном T[Size], то возвращаем true.
Аналогично реализуется и функция для беспредельного массива.
Хотя GCC может задействовать и встроенные функции __is_unbounded_array и __is_bounded_array.
Вся эта веселая возня наводит на мысль, что тип массива - это нечто большее, нежели указатель. Убедимся в этом, откроем Itanium C++ ABI.
В описании декорирования имен смотрим пунк 5.1.5.6 Array types. Типы массивов кодируются их размером и типом элементов. Заметьте, "массивные" аргументы функции кодируются как указатели. Размер массива опускается для неполных типов массивов.
Схема кодирования:
<array-type> ::= A [<array bound number>] _ <element type>
::= A <instantiation-dependent array bound expression> _ <element type>
То есть int[6] будет декорирован как A6_i. Тип сохраняется, и это очень важно для метапрограммирования.
int x[10] {};
template <class T>
void func(T t) {...} // T = int*
При вызове func(x), конечно же, никто в здравом уме не станет копировать массив по значению, как и велит ABI, массив передается по указателю. Однако, если мы используем universal reference, то тип передается точнее
template <class T>
void func2(T &&t) {...} // T = int (&)[10]
Можно и механизм перегрузки задействовать, тоже будет работать.
int sum(int (&arr) [10]) -> _Z3sumRA6_i int sum(int (&arr) []) -> _Z3sumRA_iВсегда полезно знать полную информацию о массиве, особенно когда жонглируешь шаблонами. Например, если где-то мы конструируем объект на основе переданного типа, то нужно зорко следить, чтоб не подсунули беспредельный массив. "Все это очень интересно, но только никак не относится к сути вопроса", - собеседник был явно ошарашен. Короче говоря, не самое мое удачное интервью. Хотя бы оплеванным после него себя не чувствуешь. Через пару дней, к моему удивлению, мне все же сделали интересное предложение. Без шума и пыли. Респект.
446
std::identity
Интересные своей кажущейся бесполезностью функции есть не только в compile-time. Runtime тоже может порадовать нас функтором
std::identity. На cppreference можно найти пометку, что это чудо возникло в C++20, но это не совсем так. Давным-давно SGI (Silicon Graphics, Inc) уже предоставлял identity. Если уж без функтора не обойтись, но делать ничего не надо, то identity может быть использован как холостой. Его operator() возвращает переданный аргумент без изменений.
По рассказам современников, выглядел он примерно так:
template <class T>
struct identity : public unary_function<T, T> {
T& operator()(T& x) const { return x; }
const T& operator()(const T& x) const { return x; }
};
Жила себе эта колхозная реализация где-то в functional и в ус не дула.
Однако беспокойные умы, дабы улучшить плюсы, решили ввести известную нам метафункцию в стандарт:
template <class T>
struct identity { typedef T type; };
Ну, так получилось, что именно в таком виде положили ее в utility.
Неприятности начались практически сразу же. Уже через два года (в ноябре 2007) появилась первая - проблема №700.
Документ N1856 от 26 августа 2005 года предполагает создание в заголовочном файле utility структуру identity, которая конфликтует с уже существующей нестандартной реализацией identity в functional.А что мы делаем, если видим два класса с одинаковыми названиями? Конечно! Их нужно объединить для обратной совместимости и всего делов:
template <class T>
struct identity {
typedef T type;
const T& operator()(const T& x) const;
};
Очень странно, что после такого блестящего решения обнаружились новые изъяны. Проблема №823 зафиксирована в марте 2008 года.
В документе N2588 предлагается объединить два определения, но это ломает identity<void>.Теперь стало невозможно определить
identity<void>, поскольку для этого нам понадобится ссылочный тип void, что неестественно и отвратительно.
Решить можно и эту проблему, нужно просто ввести частичную специализацию для identity<void>.
Наконец, в декабре 2008 года подоспела проблема №939, где критиковалась работа std::identity со ссылками на временные объекты.
std::identity берет аргумент T const & на входе и выдает такой же на выходе. К сожалению, это позволяет передать объект отличного от типа T, но конвертируемого в него, и вернуть ссылку на уничтоженный временный объект.
Вариантов исправлений было много. Выбран был самый оптимальный: удалить к черту, вон из стандарта! Тем более что даже std::forward больше не опирался на identity.
Так бесславно закончилось первая попытка стандартизации.
Второе пришествие std::identity состоялось уже после 2018 года, с появлением документа P0898 Standard Library Concepts.
Там тихо-мирно вводится определение дополнительного класса identity, в стандарте теперь можно найти по адресу 22.10.12 Class identity [func.identity]
Реализован в ranges_cmp.h и очень похож на неповторимый оригинал от SGI, только используется perfect forwarding для передачи аргумента.
struct identity {
template<typename _Tp>
[[nodiscard]]
constexpr _Tp&& operator()(_Tp&& __t) const noexcept {
return std::forward<_Tp>(__t);
}
using is_transparent = __is_transparent;
};
Какого рожна ranges потребовались эта бесполезная функция? Использовать этот функтор напрямую в общем-то бессмысленно. Но std::identity служит как отображение значений по умолчанию.
Да, есть такая фишка в пространстве имен ranges. Для множества алгоритмов можно указать функтор projection - вызываемый объект, применяемый к каждому элементу диапазона перед обработкой. Например:
std::vector<int> w {{1, 2, 3}};
auto projection = [](int x) { return x + 10; };
auto result = std::ranges::all_of(w, [](int x){ return x < 10; }, projection); // result == false
Здесь каждому значению будет прибавлено 10 перед сравнением. Если мы не укажем ничего вместо параметра projection, то все значения будут сравниваться с 10 напрямую.
Кстати, старую версию от SGI тоже можно найти в GCC (ext/functional). Оставили в назидание потомкам.446
std::type_identity
"Что это?" - маститый разработчик недоуменно таращился на мой код, - "решил создать самую бесполезную метафункцию в плюсовой истории?"
Тут же заливистый смех наполнил окрестности.
template<typename _Tp>
struct type_identity { using type = _Tp; };
"Зачем тебе получать значение типа, если он тебе известен заранее?"
static_assert(std::is_same_v<std::type_identity_t<int>, int>);
"Зачем оно тебе надо?" - не унимался наш местный маэстро.
"В стандарт пристрою!" - дерзко отвечаю я, устав слушать клокочущее горловое бульканье, - "я и пропозл написал!"
Последняя реплика повергает Маститого на пол, и всхлипы доносятся уже из-под стола.
"Ладно, написал не я, а Timur Doumler в 2018 году. Документ номер P0887R1..."
Смех резко стих. Из-под стола появилось красное и серьезное лицо: "Если Timur Doumler считает, что в этой бесполезности есть смысл, надо задуматься. Возможно, перед нами не кусок ерунды, а мощный инструмент метапрограммирования!"
В качестве причины, побудившей создать type_identity, уважаемый автор указывает необходимость в некоторых случаях отключить автоматический вывод типов (type deduction).
Тогда пользователь вашего кода, хочет он того или нет, будет указывать тип явно.
Например, чтоб избежать неоднозначности и заставить пользователя функции foo указывать шаблонный тип, то достаточно написать
template <class T>
void foo(std::type_identity_t<T>);
Если вызвать функцию без указания типа:
foo(1);Нарвемся на ошибку:
no matching function for call to 'foo(int)'
Укажем тип явно и все соберется без проблем:
foo<int>(1);Собственно, идея не нова. В узких кругах этот прием практикуют широко. Его даже можно назвать целой идиомой идентификации типа (
type identity idiom).
Где это может пригодиться? С тех пор как в c++17 стало возможно выводить шаблонный тип прямо из конструктора, опасность не покидает наш код.
struct smart_pointer {
smart_pointer(T *) ...
};
Такой конструктор не отличит указателя на объект от указателя на массив. Неизвестно, что за тип ему вздумается вывести. Лучше сразу придушить все вольности:
template <class T>
struct smart_pointer {
smart_pointer(std::type_identity_t<T> *) ...
};
Забавно, что в одном случае type_identity требует строгости, а в другом - сделает компилятор более сговорчивым.
Например, мы хотим сложить два значения
template <class T>
T sum(T a, T b) { return a + b; }
Когда типы аргументов разные возникает небольшое недопонимание:
sum(0.F, 0);Здесь мы хотим сложить значение
float и int, ничего криминального, но: no matching function for call to sum(float, int)
Однако переписав функцию как:
template <class T>
void sum(T, std::type_identity_t<T>);
компилятор не видит тождественности типов, и если второй тип приводится к первому, то это успех.
sum(0.F, 0); // OKНеприводимые типы все еще будут вызывать вопросы:
struct Empty {} empty;
sum(0.F, empty); // ERROR
sum(0.F, ""); // ERROR
Еще type_identity можно использовать как базовый блок метапрограммирования.
Например, можно реализовать remove_const так:
template <class T>
struct my_remove_const : std::type_identity<T> {};
template <class T>
struct my_remove_const<T const> : std::type_identity<T> {};
Никаких лишних using внутри, красота! Так и вижу, как все бросились переписывать свою личную библиотеку шаблонов...446
No сountry for negative integer literals
Представьте, связывается с вами солидная компания ЛузерДейт, зовут поговорить. Потом пропадают. Вы про них забываете и спокойно ковыряетесь в коде. Потом всплывают неожиданно, как тигровая какула в бассейне, и тянут уже на собеседование. И вот, на следующий день ровно в 6 утра начинается экзекуция.
Конечно, будет задача. Давай, говорят, напиши нам как реализовать функцию
atoi, которая перевела бы строку в число.
"Входные данные - это затрапезная си строка?" - уточняю я.
"Нул-терминэйтед си стринг", - щеголяет безукоризненным английским собеседник.
int lovely_atoi(const char *ptr) {
int result = 0;
while (*ptr) {
int dig = *ptr - '0';
result *= 10;
result += dig;
ptr++;
}
return result;
}
Одним движением мозолистой руки набираю я.
"Нет!" - доносится визг из наушников, - "на входе не char, а uint8_t!"
"А вы не заболели?" - рассуждаю я вслух, - "как потом этим пользоваться?"
Ладно, у них какие-то свои соображения, меняем аргумент
int lovely_atoi(const uint8_t *ptr)
Тут глаза интервьюеров подозрительно прищурились, они почуяли кровь. Как, говорят, вы проверите входной диапазон?
Допустим, вот так
int dig = *ptr - '0';
if (dig < 0 || dig > 9) {
panic_at_the_disco();
}
"Ага!" - взвились, как коршуны, мои собеседники, и, судя по звукам, стали скакать по столу, - "а тип-то разыменованной переменной беззнаковый! А если ее значение меньше? Видите проблему?"
Вот теперь я отчетливо видел проблему. Забыли они о явлении integral promotion! Все значения в этом выражении будут неявно преобразованы в int, и результат будет такой же. Никакого криминала тут нет, о чем забыли достопочтенные господа, прыгающие в тот момент по предметам мебели.
Когда все успокоились, мы перешли к следующей задаче. Что покажет следующая проверка?
static_assert(0x80000000 == -0x80000000); // ?
Покажет только одно - ваше присутствие на интервью нежелательно.
На самом деле выражение пройдет проверку. За разъяснениями опять обратимся к стандарту.
Раздел 5.13.2 Integer literals [lex.icon] подскажет нам, что тип целочисленного литерала выбирается согласно таблице Table 8: Types of integer-literals.
Судя по ней, целочисленные литералы в десятичном виде без суффикса сначала пробует стать int, а потом long int и т.д.
Целочисленные литералы без суффикса не в десятичном представлении после неудачной попытки стать int, пробуют себя в unsigned int, затем в long int и т.п.
Поэтому
static_assert(std::is_same_v<decltype(0x80000000), uint32_t>);
static_assert(std::is_same_v<decltype(2147483648), int64_t>);
хотя это одно и то же число.
И еще деталь. Негативных целочисленных литералов не бывает! Это минус впереди указывает, что нужно применить унарный минус к значению литерала. Тут уже 7.6.2.2 Unary operators говорит нам, как действовать. В общем, в результате получается 0x80000000, поэтому изначальная проверка будет пройдена. Жаль, что не все верят в такое объяснение.
Конечно, компания имеет полное право задавать любые вопросы и считать верными какие угодно ответы. Я не осуждаю, ведь главная цель собеседования - узнать насколько адекватна контора и люди, там работающие.
Однако это мне уже давно не стыдно за проваленные собеседования. Могу морозить чушь, как холодильник Снайге. Возможно, мне скучно отвечать, и я думаю о чем-нибудь другом, утопая в дальнем дорогом. Имею право что-то забыть и невразумительно мычать. В конце концов я даже не готовлюсь, и это привилегия соискателя. Чего интервьюеры никак не могут себе позволить.446
std::__select_int::_Select_int_base
"Загадывай свою загадку, я готов!" - плюхнувшись на стул, заносчиво произнес разработчик Ефпидрифий.
Ему не терпелось завершить аттестацию и впрыгнуть в новый грейд ведущего разработчика категории 5 подкатегории 3. Это был бы головокружительный скачок на целых две подкатегории!
Напротив него сидела товарищ Сфинкс. Абсолютно неподвижная, словно статуя, сквозь каменный покерфейс которой не решалась показаться ни одна эмоция.
"Хорошо", - отчеканила Сфинкс, - "скажите мне, какой тип следует использовать для переменной индекса?"
"Ну, это же очевидно!.." - рассмеялся разработчик и начал свой рассказ.
С его слов, ответ на этот вопрос таится в
std::variant, словно игла в яйце. Этот тип содержит список типов, который формируется во время компиляции, и ему остро необходим индекс, чтоб знать, объект какого тип сейчас на коне. Этот индекс, он же тэг, имеет весьма хитрое объявление.
Очевидно, для std::variant<char, char>, все значения тэга лежат в диапазоне от нуля до единицы. Тут в качестве типа подойдет uint8_t.
using VariantCharType = std::variant<char, char>;
static_assert(sizeof(VariantCharType) == 2);
Однако, если напихать в список еще char, чтоб их стало больше 256, то std::variant просто увеличится в размерах.
using VariantChar256Type = std::variant<Types256>; // Types256 это 256 char-ов
static_assert(sizeof(VariantChar256Type) == 4);
Здравый смысл подсказывает, что тип индекса стал uint16_t, а из-за выравнивания размер скакнул в два раза.
Как он это делает? Что скрывает в своей ненасытной утробе?
Опять же, ноги растут из якобы бесполезного bits/parse_numbers.h. Есть там один небезынтересный шаблон _Select_int_base для определения подходящего численного типа.
Его самая интересная частичная специализация:
template<unsigned long long _Val, typename _IntType, typename... _Ints>
struct _Select_int_base<_Val, _IntType, _Ints...>
: conditional_t<(_Val <= std::numeric_limits<_IntType>::max()),
integral_constant<_IntType, _Val>,
_Select_int_base<_Val, _Ints...>> {};
Выражение conditional_t<(_Val <= std::numeric_limits<_IntType>::max())... четко указывает, что если число _Val убирается в диапазон типа _IntType, то выбираем его, если нет, то выбрасываем этот тип и повторяем итерацию. Поэтому важно, чтоб возможные типы шли от самого узкого до самого широкого.
В самом std::variant все сделано правильно:
template <typename... _Types>
using __select_index =
typename __select_int::_Select_int_base<sizeof...(_Types),
unsigned char,
unsigned short>::type::value_type;
Значение имеет только количество типов в шаблонных аргументах. Здесь __select_int выбирает между uint8_t и uint16_t, если типов меньше 256, то первый тип, иначе второй. Больше вариантов нет. Разработчики справедливо считают, что если вы попытаетесь запихать в параметры больше 65535 типов, то что-то не в порядке, как минимум с головой.
На самом же деле, главная метафункция здесь _Select_int, ее предполагалось использовать в литералах.
template<char... _Digs>
using _Select_int;
То есть можно во время компиляции определить подходящий тип для переменной:
using T100 = std::__select_int::_Select_int<'1','0','0'>::type::value_type;
static_assert(std::is_same_v<T100, uint8_t>);
using T1000 = std::__select_int::_Select_int<'1','0','0','0'>::type::value_type;
static_assert(std::is_same_v<T1000, uint16_t>);
Может быть, кому-то это пригодится.
Сфинкс молча, ликуя и скорбя, смотрела на Ефпидрифия. Она только вчера прочитала об этом трюке в малоизвестном телеграм-канале и не ожидала правильного ответа. Теперь придется исполнять со скалы бейсджампинг. Таков был уговор. Разработчик же хитро щурился, ему очень повезло, что буквально вчера он подписался на один малоизвестный телеграм-канал.446
std::__parse_int
Если вам кажется, что вы занимаетесь полной ерундой на никому не нужном, богом забытом проекте в арьергарде технологического декаданса, не грустите. Вспомните, что в GCC есть файл
bits/parse_numbers.h интереснейшего содержания. Там размазана на пару сотен строк матафункция _Parse_int, которая существует исключительно для того, чтоб получать числа из набора символов.
Соответствующий класс объявлен так:
template<char... _Digs>
struct _Parse_int;
Здесь _Digs - это аргумент шаблона, пакет значений char из которых и состоит искомое число.
Например, если в пакете у нас '1' и '0', то эта хитрая бестия слепит число 10.
using _Val = std::__parse_int::_Parse_int<'1', '0'>;
static_assert(_Val::value == 10U); // OK
Все эти архиважные вычисления происходят во время компиляции, что невероятно удобно, но странно.
"Однако же, какая на редкость бесполезная метафункция!" - подумал я, когда впервые увидел это чудо. Потом задумался. В плюсах ни одна дичь не творится просто так. Разработчики GCC кринжатины не навалят перед дверью за здорово живешь, по недомыслию, только холодный расчет!
Стандарт же на вопрос: "Оно тут зачем?", отвечает уклончиво.
В разделе 12.6 User-defined literals [over.literal], стих 5 можно найти такие чарующие строки:
Шаблонный числовой литеральный оператор — это такой шаблон, список параметров которого содержит единственный параметр, но не типовой, а пакет элементов типа char.
То есть если мы хотим сделать пользовательский литерал для чисел, то сделать его в шаблонном виде можно только в такой форме:
template<char ... _Digits>
double operator ""_x();
Для строк в с++20 появится своя форма шаблонного литерального оператора, но это уже другая история.
Тогда при попытке создать литерал _x мы добьемся вызова оператора operator""_x():
10_x -> double operator""_x() [with char ..._Digits = {'1', '0'}]
Чувствуете, куда ветер ветку клонит, чего дубравушка шумит? Тут может пригодиться _Parse_int! И уже пригодилось. В GCC мы можем найти использование этой метафункции в bits/chrono.h.
Для временных литералов может быть полезно проверять значение на осмысленность. Допустим, мы хотим сделать литерал для обозначения часов в сутках.
struct Hour {
uint32_t value;
};
К сожалению, больше 24 часов в сутках быть не может, поэтому литеральный оператор запишем следующим образом:
template <char... _Digits>
constexpr Hour operator""_Н() {
using _Val = std::__parse_int::_Parse_int<_Digits...>;
static_assert(_Val::value < 24U, "literal value is wrong!");
return Hour {_Val::value};
}
Теперь если мы попытаемся создать литерал 10_H, то все пройдет гладко, но стоит написать 36_H, как все рухнет:
error: static assertion failed: literal value is wrong! static_assert(_Val::value < 24U, ...А еще
_Parse_int понимает разные формы записи. 10_H и 0xA_H даст одно и то же значение.
В общем, не такая уж и бесполезная функция. Особенно для любителей создавать свои типы физических единиц, что, конечно, правильно и одобряется AUTOSAR.
Очень полезная функция, полезнее меня на этом багом забитом проекте...446
std::is_literal_type
Литералы появилось очень давно, наверное, еще во времена альбигойских войн. Вот литеральные типы - изобретение относительно свежее, появившееся только в стандарте с++11. В одном из последних черновиков стандарта в главе
6.8 Types, разделе 6.8.1 General [basic.types.general] дается пространное и исчерпывающее определение литерального типа.
Оказывается, литеральными считается и тип void, и скалярные, ссылочные типы, и даже массивы литеральных типов. Также литеральными могут стать классы, если им сделать тривиальный или constexpr деструктор (есть и такой в с++20), и если все нестатические члены класса, а также базовые классы имеют литеральный тип. Сюда в с++17 попадают лямбды. На иные классы накладываются ограничения: объединения должны иметь хотя бы один член литерального типа. Если обычный класс имеет вариантные члены, то они должны удовлетворять вышеизложенным требованиям. Самое главное, что класс должен иметь хотя бы один constexpr конструктор или шаблонный конструктор. Копирующий или перемещающий конструктор не в счет.
Фух, в примечании к этому многословному определению человеческим языком поясняется, что литеральный тип — это такой тип, объект которого можно создать в константном выражении. Впрочем, гарантий такое определение не дает.
Другими словами, мы ожидаем, что литеральный тип можно использовать в constexpr выражениях. Если признаки литеральности известны, то почему бы не сделать соответствующую метафункцию std::is_literal_type, которую можно было бы использовать в обобщенном программировании.
Праздник метапрограммирования нам испортил некий мистер Alisdair Meredith, написав в 2016 году работу Deprecating Vestigial Library Parts in C++17. В разделе Deprecate the is_literal Trait он потребовал убрать этот трейт из стандартной библиотеки.
"Уберите же класс свойств is_literal!" - грозно кричал он со страниц статьи. - "Класс свойств is_literal имеет исчезающе малое значение для метапрограммирования, поскольку на самом деле мы хотим быть уверены, что конкретная конструкция инициализируется константно. Определение литерального типа, где требуется наличие хотя бы одного constexpr конструктора, слишком слабо, чтобы его можно было использовать осмысленно."
Однако даже автор признает, что текущая реализация этой метафункции вреда не приносит, и можно ее оставить, пока комитет не примет решение об окончательном удалении. Работал же когда-то std::variant с этом внутри и ничего, не треснул.
Через два года, в 2018, наш знакомец в составе группы лиц (Alisdair Meredith, Stephan T. Lavavej, Tomasz Kamiński) по предварительному сговору опубликовали еще одну работу Reviewing Deprecated Facilities of C++17 for C++20, где таки вознамерились удалить совсем все малополезные классы свойств в том числе и is_literal_type.
В качестве обоснования они написали, что классы имели ненадежные или неудобные интерфейсы. Метафункция is_literal_type вовсе не предоставляла возможности определить, какое подмножество конструкторов и функций-членов типа было объявлено constexpr.
И могло получиться так, что программа, которая полагается на свойства класса is_literal_type или на шаблон is_literal_type_v, может не скомпилироваться.
Допустим, есть у нас класс литерального типа:
struct A {
constexpr A() : m{} {}
A(int i) : m{i} {}
int m;
};
Да, все требования литеральности класс соблюдает: деструктор тривиальный, тип члена m литеральный, constexpr конструктор в наличии.
static_assert(std::is_literal_type_v<A> == true);
Можем ли мы тогда определить такую constexpr функцию?
constexpr int FUNC(int i) {
A a{i};
return i + a.m;
}
Да, но есть нюанс. Если мы попытаемся использовать ее во время компиляции, то получим ошибку, о чем нас и предупреждали.
static_assert(FUNC(1) == 2);
Ежели здесь мы не можем положиться на is_literal_type, то зачем оно надо?
В общем, определение литерального типа есть в стандарте, поэтому в языке еще существует std::is_literal_type, однако в с++20 он официально признан удаленным, поэтому искать его следует в разделе 16.4.5.3.2 Zombie names [zombie.names]. Такой вот, понимаешь, кадавр.446
interview.part2
Человек слаб. Стоит ему увидеть кликбейтный заголовок "Переложи одну спичку так, чтоб..." или "Продолжи последовательность", как тут же все важные дела забыты, зато туда-сюда перекидывается несчастная спичка, и многострадальная последовательность препарируется на предмет закономерности. Так и на собесе совершенно невозможно отказаться от задачи, ведь она словно игрушка в шоколадном яйце. Каждый раз надеешься найти что-то интересное, но сейчас не повезло. Оказалось, надо было всего лишь обнаружить цикл в односвязном списке.
"Наверняка взял из сборника 1000 занимательных алгоритмов", - скривившись подумал я, - "А когда-то волны Зоммерфельда рассчитывал, как низко я пал..."
Я уныло посмотрел на ноут, стоявший перед Руководителем. Он, перехватив мой взгляд, быстро пододвинул устройство к себе. Бумаги тоже не нашлось, зато интерактивных досок было в избытке. Запишем элемент списка:
struct Node {
Node *next;
};
Тут, говорю с видом знатока, классическая проблема обедающих в маршрутке философов. Им передают за проезд купюру, они передают ее друг другу по кругу. Потом к ним приходит куча мелочи, которую пересыпать тяжелее, поэтому ее они передают друг другу медленнее. Рано или поздно купюра и мелочь окажется в руках у одного из философов, он, раззява, все уронит. По звону монет мы поймем, что цикл есть:
bool check_boring(Node *node) {
bool is_cycled = false;
std::pair<Node *, Node *> ptr {node, node};
bool step = false;
while (ptr.first && !is_cycled) {
ptr.first = ptr.first->next;
ptr.second = step ? ptr.second->next : ptr.second;
step = !step;
is_cycled = ptr.first == ptr.second;
}
return is_cycled;
}
Еще не успев дописать решение, я заметил, что некоторые стали откровенно скучать и уткнулись в смартфоны.
"Вообще-то, можно обойтись без второго указателя," - громко сказал я, чтоб стало еще веселее. Народ перестал тупить в телефон, а кто-то, кажется, описался от неожиданности.
Поскольку адреса будут выровнены по размеру указателя, то как минимум два последних бита будут нулевые всегда. Поэтому их можно использовать, чтоб пометить пройденный узел. Встретив такой узел, поймем, что цикл есть.
Сварганим небольшой класс:
template <class T> struct MarkPointer {
using Pointer = T *;
MarkPointer(Pointer *ptr) : ptr_{ptr} {}
~MarkPointer() { *ptr_ = reinterpret_cast<Pointer>((AsNumber() >> 1) << 1); }
bool IsEnd() const { return *ptr_ == nullptr; }
bool IsMarked() const { return (AsNumber() & 1) != 0; }
uintptr_t AsNumber() const { return reinterpret_cast<uintptr_t>(*ptr_); }
MarkPointer MarkAndNext() const {
Pointer *next = &((*ptr_)->next);
*ptr_ = reinterpret_cast<Pointer>(AsNumber() | 1U);
return {next};
}
Pointer *ptr_;
};
MarkPointer принимает указатель на объект типа Pointer, потому как мы вознамерились этот объект менять. Если Pointer указывает на nullptr, то мы нашли конец списка, метод IsMarked определяет, помечен узел или нет. Самый интересный метод MarkAndNext - порождает новый объект на основе указателя на следующий узел списка, одновременно помечая текущий узел. Теперь, когда у нас есть такой мощный объект, осталось только правильно его применить.
template <class T> bool check(T *ptr) {
MarkPointer p {&ptr};
return check_impl(p);
}
Функция check создаст обертку для первого элемента в списке и вернет результат функции check_impl.
template <class T> bool check_impl(MarkPointer<T> const &pointer) {
return !pointer.IsEnd() && (pointer.IsMarked() || check_impl(pointer.MarkAndNext()));
}
Эта функция проверяет не достигнут ли конец списка, если нет, то проверяет, помечен ли узел. Если не помечен, то зацикленности не обнаружено, и можно вызвать check_impl для следующего элемента списка. Да, рекурсивно. Нет, адреса не будут испорчены после этого. Деструктор MarkPointer уберет нашу метку, вернув ваш драгоценный и бесполезный список в исходное состояние.
Руководитель направления сполз под стол и сомлел. Двое его подручных корчились на полу. Еще один в слезах убежал звать охрану.
"Пора удирать", - подумал я и был таков.446
interview.part1
Наконец-то! Пригласили посмотреть офис большой китайской компании Ойвэй. Насчет интервью я особо не беспокоился, беседовал с несколькими командами и все проходило отлично. Однако оказалось, что не все команды умеют собеседовать одинаково хорошо.
Началось все приемлемо, встретили меня почти сразу. Лифт поднял нас на 13 этаж. Как только мы вышли, сопровождающая моя, заговорщицки подмигивая, прошептала, что ждут нас на 14. Вздохнув об ушедшей кабине, я поплелся вверх по лестнице. После того, как я оставил автограф в книге почетных гостей (в паспорт не смотрели, поэтому написал: "Довлатов С.Д."), меня без лишний церемоний впихнули в просторный зал для демонстраций, где на стенах висели огромные интерактивные доски, а под потолком были прибиты несколько проекторов. На полу уже сидели двое. Не вставая с места они поздоровались безжизненным голосом, я тоже поприветствовал их и сказал, что очень приятно. Хотя было не очень. Приземлившись рядом с новыми знакомыми, мы начали молчать. Изредка в зал вползали новые участники встречи и, уже не здороваясь, занимали лучшие места у батарей.
Через некоторое время в зал вихрем ворвался Руководитель с ноутбуком на голове.
- Вы работали с FreeRTOS? - спросил он вместо приветствия.
Я испытал глухое раздражение.
- Работал.
Насколько я понял, их проект с ОСВР не был связан и не планировал связываться.
- Тогда сможете сказать, что это такое?
Я поймал на себе иронический взгляд. Очевидно, резюме тут заранее не читали, а написанному не верили. Вдруг, мол, самозванец...
- Давайте, - не выдержал я, - прекратим этот идиотский экзамен. Я прочитал всего Таненбаума (тут я немного преувеличил, ограничился только "Современными операционными системами"). В общем, разбираюсь...
- И все-таки? - Руководитель ждал ответа. Причем того ответа, который ему был заранее известен.
- Ладно, - говорю, - попробую... Что ж, хоть и лучшей стратегией для построения архитектуры и является SuperLoop на bare-metal, однако порой кооперативной многозадачности становится явно недостаточно для поставленных задач, например, для управления сложными производственными процессами. Тут нужны именно операционные системы, для которых время является ключевым параметром. Вот тут нам и пригодится фриртос...
- При чем тут супер луп? И причем тут баре-метал?
- Ни при чем! - окончательно взбесился я. - И баре-метал ни при чем, а Фриртосом звали одного из мушкетеров: Атос, Портос и Фриртос.
- Успокойтесь, - прошептал Руководитель направления, - вы какой-то нервный... Давайте задачку порешаем, а?
- Задачку?! - взревел я, - А какую? - спросил я уже спокойным голосом.
Однако выражение лица решил сохранять прежнее - человека, внезапно унюхавшего кучу дерьма. Бывает, что с первой фразы уже по интонация четко определяешь, сработаешься ли с командой. Если нет, то соглашаться даже на жирный оффер не стоит. Иначе через полгода вы будете гоняться за ними по узким коридорам с шокером. А может они за вами...
A suivre.
446
std::__detail::__variant::_Uninitialized
На первый взгляд
std::variant может показаться скучнейшим стандартным контейнером, но стоит только вглядеться в исходный код, тут же вы приметите следы бушующих внутри страстей. Особенно интересно было наблюдать сражения за свойство is_trivially_destructible. О, сколько велосипедов было сломано в этой борьбе! Как известно, если переданные типы Types... тривиально разрушаемы, то и std::variant<Types...> наследует это свойство. В этом случае нет нужды заботиться о деструкторе вообще. Иначе есть нюансы.
Сидящая внутри структура _Variadic_union вынуждена работать с любым типом, то есть деструктора у нее вроде и не должно быть, но с нетривиальными типами изволь работать без проблем. Напрямую реализовать это таки проблематично.
union R {
int t;
std::string str;
};
Вспомним, что у таких объединений скрыто удален конструктор и деструктор по умолчанию, и создавать такой объект невероятно трудно. Выход есть! Стандартная библиотека на этот случай выдавила из себя вспомогательный класс-обертку _Uninitialized:
template<typename _First, typename... _Rest>
union _Variadic_union<_First, _Rest...> {
...
_Uninitialized<_First> _M_first;
_Variadic_union<_Rest...> _M_rest;
};
_Uninitialized<T> гарантированно будет литеральным типом, даже если T не является таковым. К слову, литеральные типы - это типы, которым позволено быть constexpr, их можно создавать таковыми, производить операции над ними и даже возвращать из constexpr функций.
В комментарии к этому удивительному решению слышатся жалостливые стоны неизвестного разработчика о том, что некий пункт 10.5.3 в [basic.types] не реализован (тут имеется в виду документ N4606 2016 года) и объединение даже с литеральными типом на борту не считается литеральным типом. Вот когда это свойство реализуют, он тут же уберет этот костыль. Ха, он и поныне там!
template<typename _Type, bool = std::is_literal_type_v<_Type>>
struct _Uninitialized;
template<typename _Type>
struct _Uninitialized<_Type, true> {
template<typename... _Args>
constexpr _Uninitialized(in_place_index_t<0>, _Args&&... __args)
: _M_storage(std::forward<_Args>(__args)...) { }
...
_Type _M_storage;
};
template<typename _Type>
struct _Uninitialized<_Type, false> {
template<typename... _Args>
constexpr _Uninitialized(in_place_index_t<0>, _Args&&... __args) {
::new (&_M_storage) _Type(std::forward<_Args>(__args)...);
}
...
__gnu_cxx::__aligned_membuf<_Type> _M_storage;
};
Тут же в следующих строчках появляется предположение, что мы не захотим удалять _Uninitialzied<T>, поскольку такая обертка делает тип тривиально разрушаемым в любом случае, независимо от типа T. Для нетривиального и нелитерального T в _Uninitialized<T> используется некий специальный буфер __aligned_membuf, что резервирует память для объекта и позволяет создать объект позже, по требованию через placement new.
Чуть позже все пошло наперекосяк: в с++17 std::is_literal_type признали порочной метафункцией, а в с++20 вообще удалили с концами.
Разработчики попытались напрямую привязаться к is_trivially_destructible_v:
template<typename _Type, bool = std::is_trivially_destructible_v<_Type>>
struct _Uninitialized;
Эта конструкция гарантированно является тривиально разрушаемым типом, даже если T таковым не является. Это работает для с++17, а в следующей версии стандарта оказалось, что это свойство не важно для _Variadic_union. Была даже попытка реализовать структуру _Uninitialized через union, просто прицепив пользовательский деструктор, но выглядело это как челюсти Чужого. В с++20 легче же дописать пользовательский деструктор прямо для _Variadic_union, чем упражняться с шаблонами.
constexpr ~_Variadic_union() requires (!__trivially_destructible) { }
Если один из типов нетривиально разрушаемый, тогда добавляем деструктор через концепт __trivially_destructible. На самом деле мы разрушим объект уровнем выше в функции _Variant_storage::_M_reset(), единообразия для. Зато с _Uninitialized можно не заморачиваться и оставить самый простой вариант для всех типов.446
std::variant
Видел я некоторые реализации стандартной библиотеки, но только исследование исходников GCC вызывает стойкую ассоциацию с рысканием по свалке. В хорошем смысле, конечно. В одном месте вещицу забавную обнаружишь, к себе в проект утащишь, в другом - велосипед найдешь, точь-в-точь такой ты изобрел 10 лет назад! Вот вчера порылся опять, нашел красивый
std::variant, тонкая работа, добротная, сейчас таких уже не делают. Дай, думаю, и вам покажу, что в GCC он сделан на основе объединения и индекса активного элемента.
template<typename... _Types>
class variant
...
_Variadic_union<_Types...> _M_u;
__index_type _M_index;
Только вот объединение у нас не простое, а шаблонное _Variadic_union, в качестве шаблонных аргументов передаются все типы из std::variant, естественно.
Стандарт в разделе 11.5 Unions [class.union] стихе первом говорит нам: A union is a class defined with the class-key union.
А раз union это класс, то почему мы ему не быть шаблоном?
Если хорошенько подумать, это дико удобно. Есть возможность по индексу достучаться до любого элемента объединения даже во время компиляции. Кстати, в качестве индекса удобней брать не просто число, а целый тип, например, стандартный std::in_place_index_t, так называемый тег устранения неоднозначности. На самом деле этот индекс-тэг очень просто устроен, он даже не приводится автоматически к целому значению, как std::integral_constant.
template<size_t _Idx> struct in_place_index_t {
explicit in_place_index_t() = default;
};
template<size_t _Idx>
inline constexpr in_place_index_t<_Idx> in_place_index{};
Сойдет и так! В лучших традициях функционального программирования, наше шаблонное объединение - это рекурсивно воссоздающаяся структура. Давайте максимально упростим конструкцию, чтоб лучше рассмотреть суть:
template <class ... Types>
union _Variadic_union {
};
Это общее определение, настолько неконкретное, что сработает, только когда типов в Types... не останется вовсе.
А вот его частичная специализация работает всегда, если есть хоть один тип в пакете.
template <class Head, class ... Tail>
union _Variadic_union<Head, Tail ...> {
template <class T>
_Variadic_union(in_place_index_t<0>, T && t) : first {t} {}
template <int N, class T>
_Variadic_union(in_place_index_t<N>, T && t) : rest {in_place_index<N-1>{}, t} {}
Head first;
_Variadic_union<Tail...> rest;
};
Здесь берется первый тип Head из пакетика и декларируется первый элемент объединения этого типа. Вторым элементом, который воссоздается на этом же адресе, конечно, будет другое шаблонное объединение _Variadic_union<Tail...> rest. То есть тот же _Variadic_union, но в качестве шаблонного параметра мы передаем оставшийся пакет без первого типа Head.
Тогда, если мы захотим сделать такое объединение (допустим, что у нас есть конструктор по умолчанию),
_Variadic_union<int, float, X> var {};
будет вызван только конструктор по умолчанию _Variadic_union<int, float, X>::_Variadic_union(). Члены объединения останутся неинициализированными.
Однако если мы очень хотим сразу обозначить активный элемент, то стоит явно это указать:
_Variadic_union<int, float, X> var {in_place_index<1>{}, 3.14F};
Здесь сначала будет вызван конструктор
_Variadic_union<int, float, X>::_Variadic_union(in_place_index_t<1>, 3.14F)
Судя по ненулевому индексу, следует вызвать конструктор объекта rest:
_Variadic_union<float, X>::_Variadic_union(in_place_index_t<0>, 3.14F)
Тут счетчик в типе индекса наконец-то равен нулю, и мы благополучно инициализируем первый элемент.
Это распространенный прием приведения в соответствие шаблонного элемента к индексу, std::tuple использует похожий метод индексации.
То же работает и с инициализацией самого std::variant, можно сразу и явно выбрать активный элемент через индекс-тэг.
std::variant<float, int, X> f{std::in_place_index<1>, 42};
Тем самым устранив возможную неоднозначность. Можно и без тэгов, но об этом потом. Watchdog гонит меня со свалки.