C++ Embedded
424 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
packed structure
Собравшиеся представители компании "ООО МУЛАТ" так и не соизволили показать свои лица, я демонстративно выключил камеру. Кажется, с этого момента беседа не заладилась.
У коллективного собеседующего явно есть какая-то тактика, и он ее придерживается: то про структуру вопрос, то про управление выравниванием. Ходит вокруг да около, вот уже дрожит коллективный голос, мочи нет, как хочет говорить про свою боль, свою прелесть, свои натруженные упакованные структуры. Священный Грааль всех телеком-компаний.
Для чего они вообще нужны? Представим, что есть у нас пакет, где лежат не тюльпаны и не лилии, но данные: тэг типа int8_t и значение типа int32_t. Мы берем их и изо всех сил передаем по сети или еще как-нибудь. Допустим, endianness у нас такой, какой надо. На другом устройстве мы получаем пакеты данных и хотим их быстро распарсить.
Для этого мы уже и соответствующую структуру подготовили:
struct Pack {
int8_t tag;
int32_t value;
};
и функцию для изъятия значения:
int32_t getValue(Pack *p) {
return p->value;
}
Презрев все дурацкие правила и перекрестившись, просто натягиваем структуру на память, как сову на глобус.
Pack *ptr {reinterpret_cast<Pack*>(mem)};  // mem - это адрес полученных данных.
int32_t const x {getValue(ptr)};
Приглядевшись к функции getValue для архитектуры Cortex M33 (-mcpu=cortex-m33), мы увидим
    ldr     r3, [r7, #4]
ldr r3, [r3, #4]
Первая инструкция LDR загружает в R3 адрес начала структуры, а вторая загружает в тот же регистр значение int32_t со смещением в 4 байта от полученного адреса. Данных пришло 5 байт, а в структуре аж целых 8, проклятое выравнивание!
Понятно, что это прямой путь к катастрофе. Вот тут, улюлюкая и пританцовывая, выскакивают просветленные сишники со своими пакетированными структурами.
Им нравится доминировать и навязывать структурам выравнивание, больше всего они любят побайтовое, чтоб члены слиплись, как пельмешки, в один бесформенный комок.
#pragma pack(n) - устанавливает новое выравнивание n для всех структур ниже. Нужно не забыть вернуть выравнивание на место инструкцией #pragma pack().
Чтоб избирательно уродовать конкретную структуру в GCC, им можно задать специальный атрибут:
struct __attribute__((packed)) Pack { ...
Для IAR есть другая форма записи:
struct __packed Pack { ...
Вот теперь размер структуры такой, какой нужно, и наш ужасный код заработает. Но какой ценой?!
Почти никакой, только изменится размер смещения для второй инструкции LDR. Еще у нас появится милая пометка, что операция будет происходить по невыровненному адресу.
    ldr     r3, [r7, #4]
ldr r3, [r3, #1] @ unaligned
Да, инструкции LDR и прочие совершенно спокойно работают с невыровненными адресами, но это для Cortex M33.
Для иной архитектуры все будет не так гладко. Есть такие, что не могут кормить инструкции неровными адресами, например, Cortex M0.
Заменим у компилятора флаг на -mcpu=cortex-m0 и насладимся метаморфозами внутри функции getValue:
    ldr     r3, [r7, #4]
ldrb r2, [r3, #1]
ldrb r1, [r3, #2]
lsls r1, r1, #8
orrs r2, r1
ldrb r1, [r3, #3]
lsls r1, r1, #16
orrs r2, r1
ldrb r3, [r3, #4]
lsls r3, r3, #24
orrs r3, r2
Компилятор создает более сложный и медленный код, чтоб получить доступ к значениям в структуре и избежать HardFault. И это еще, можно сказать, повезло! Некоторые недалекие компиляторы не умеют генерировать код, который корректно работает с опакеченными структурами. Тогда попытка доступа к памяти по невыровненному адресу закончится печально.
Коллективное сознание "ООО МУЛАТ" осталось недовольно моими путанными объяснениями и снизошло уточнить, что размер упакованной структуры равен сумме размеров ее членов. Вот тут я и понял, что пора бежать. Как они могли забыть про вырожденный случай пустой структуры? Как такую ни упаковывай, размер по-прежнему будет равен единице.
👍6🔥2👏1
Designated initialization
"Привет, тигры!" - громко поздоровался с командой ее лидер, протиснувшись в маленький загон из четырех кубиклов.
Тигры медленно подняли усталый взгляд на возмутителя спокойствия.
"Сергей, ты же знаешь, что закон о запрете цирков с животными уже обсуждают?" - послышался голос из дальнего угла.
"Не надейтесь, котики, нашу команду мечты это не коснется!" - тимлид усмехнулся, - "Кому-то еще через горящие кольца прыгать за сломанную сборку".
В дальнем углу угрожающе зарычали. Никто билд не ломал, его сломало обновление cmake. Новая версия оказалось настолько прогрессивной, что самовольно выкинула из сборки IAR маленький флаг, включающий расширения компилятора. Внезапно оказалось, что мы их использовали, сами того не подозревая.
Например, есть у нас простая структурка
struct Point {
int32_t x;
int32_t y;
};
В одном месте коллеги инициализируют ее привычным для способом, перекочевавшим из языка Си.
Point p = { .x = 1, .y = 1 };
Никто особо не задумывался над правильностью данной конструкции. Работает же и для IAR, и для GCC. Но плюсы - это вам не какой-то там Си, такая запись некорректна. Для c++17 уж точно, а в IAR стандарта выше нет. Только в с++20 появляется возможность инициализировать агрегатный класс через обозначения (designators).
Поэтому при отключенных расширениях в IAR грустно наблюдаем такую картину:
Error[Pe029]: expected an expression
Вообще, инициализация структур через обозначения (designated initializaion) появляется в стандарте C99 языка Си. Народ ликовал, ведь теперь можно было не опасаться ошибок, связанных с порядком аргументов при инициализации объектов. Можно было писать как Point p = {.x = 0, .y = 0}; так и Point p = {.y = 0, .x = 0};
Тем не менее, ни в c++11, ни в c++17 стандарт инициализация через обозначения не попала. Отчасти по техническим причинам, немного по идеологическим.
Включите флаг -pedantic для GCC и вы поймете, что стандартом там и не пахло. Чистейший произвол компилятора!
К счастью, в с++20 удалось преодолеть разногласия, технические трудности и затащить это в стандарт. Однако, как всегда, есть нюанс.
Теперь в плюсах можно инициализировать агрегатные классы через обозначения, но только в порядке их объявления в этом классе. То есть только такая форма допустима: Point p {.x = 0, .y = 0}; Поменяйте местами .x и .y и получите ошибку:
Error[Pe2904]: out-of-order initializers are nonstandard in C++
Смешивать обычную инициализацию и инициализацию через обозначения строжайше запрещено. Point p { 0, .y = 0 }; приведет к таким воплям компилятора:
Error[Pe2903]: mixing designated and non-designated initializers is nonstandard in C++
И заявляю со всей ответственностью, это очень хорошо! Возьмем хрестоматийный уже пример:
struct {int sec,min,hour,day,mon,year;} z = {.day=31,12,2014,.sec=30,15,17};
Для языка Си это работает, но результат был для меня неожиданным, z содержит {30,15,17,31,12,2014}. Значение z.min равно 15, а не 12! Обозначение .day как бы задает, откуда начинать инициализацию, и число 12 будет присвоено следующему за day члену mon. Такая инициализация по кругу не бегает, если .sec не написать явно, то все члены до day получат нулевые значения.
Все это контринтуитивно и рано или поздно доведет до греха. Лучше уж явно задавать все члены структуры. Чем понятнее, тем безопаснее.
Приличные компиляторы ждать стандарт не собирались и включили эту инициализацию в свои расширения. Вообще, в складках компилятора можно обнаружить много всего интересного...
👍7🔥1
Variadic Macros
Тот неловкий момент, когда тебя попросили представить ситуацию: "Ты очнулся голый на полу общественного туалета, в руке зажаты сто рублей", а тебе и выдумывать ничего не надо!
Именно так себя чувствует разработчик, если оставить его наедине с суровым стандартом без поддержки компилятора с удобными расширениями.
После былинного отказа системы сборки, когда пропадет важный флаг компилятора (-e для IAR), можно узнать, как много удивительного кода написано не в полном соответствии со стандартом.
Например, сломается любимый макрос с переменным количеством параметров. У всех же есть такой макрос? Какая-нибудь хитрая отладочная печать... свой PRINT. И вот, вдруг перестали компилироваться все его вызовы с единственным аргументом: PRINT("hello");
Сборка заканчивалась трагическим фиаско:
Error[Pe054]: too few arguments in invocation of macro "PRINT"
Смотрим, что же представляет собой макрос PRINT:
#define PRINT(format_string, ...) \
do { \
printf("%s:%d: " format_string "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
} while (0)
О, дух старой школы! Макрос удобен тем, что внутри можно раскрыть значения __FILE__ и __LINE__; и мы увидим имя файла и номер строки, где, собственно, этот макрос и был оставлен. Внутри функции раскрывать эти значения бесполезно.
Зачем только ставить двойной октоторп перед __VA_ARGS__? Думаю, многие помнят, что это оператор вставки токена. Думаю, многие знают, что этот нехитрый прием позволяет избежать неприятностей, когда в макрос не передается никаких аргументов.
Вообще, стандарт Cи не позволяет оставлять списки переменных пустыми. Запятая мешает.
Например, есть такой макрос:
#define DBG(format_string, ...) ptintf(format_string, __VA_ARGS__)
Выглядит невинно, но если полностью игнорировать список аргументов, то вызов DBG("hello") раскроется в printf("hello",) с повисшей запятой, что недопустимо.
Чтобы решить эту проблему, компиляторы обучены особым образом обрабатывать оператор вставки токена ## для макроса с переменным количеством аргументов __VA_ARGS__. Причем это расширение компилятора есть как в GCC, так и в IAR, что совершенно чудесно. Мы с чистой совестью можем использовать это.
PRINT("hello") раскроется в printf("%s:%d: " "hello" "\n", FILE, LINE);
Если аргументы опущены, оператор ## заставляет препроцессор удалить запятую перед ним. Если аргументы в макросе присутствуют, то при вызове макроса, этот же оператор помещает аргументы после запятой. То есть конструкция , ##__VA_ARGS__ схлопывается в ничто при отсутствии аргументов и ведет себя как , __VA_ARGS__ в противном случае.
Выглядит это чище, чем любые неуклюжие попытки привести код в соответствие со стандартом. Они настолько нелепы и безобразны, что даже не пытайтесь представить! Эти картины сведут вас с ума! Я потом сам вам все покажу. A suivre.
👍6🔥1
четкий PRINT macro
Многие из вас просили не показывать жуткие способы изготовления макроса PRINT, который бы полностью соответствовал стандарту. Могу понять опасения схватить изрядную долю не просто ароматного кода, но фонящего так, что можно получить ожоги роговицы глаз. Однако врожденная токсичность позволяет мне исполнить это. Уберите детей и беременных питонистов от экрана!
Так вот, из страха или врожденного перфекционизма некоторые особо умные люди не желают использовать удобное расширение компилятора и предлагают разделить список аргументов. Короче говоря, записать макрос печати как-то вот так:
#define PRINT(...) printf("[%s:%d] " HEAD(__VA_ARGS__) "\n", __FILE__, __LINE__ REST(__VA_ARGS__))
Отрубить у списка аргументов голову, потому как там обычно идет строка форматирования, и добавить что-нибудь от себя. Остаток списка аргументов пришпилим прямо к аргументам __FILE__ и __LINE__ как хвост ослику Иа. Добавим REST(__VA_ARGS__) именно без запятой, только через пробел. Как вы уже догадались, запятую поставит макрос REST, если длина списка аргументов будет больше единицы.
Возможно ли это реализовать, не прибегая к черной магии?
Макрос HEAD реализовать несложно:
#define HEAD_HELPER(head, ...) head
#define HEAD(...) HEAD_HELPER(__VA_ARGS__, dummy)
Основную работу делает, как всегда, HEAD_HELPER - выделяет первый аргумент из переданных. Загвоздка в том, что согласно стандарту, при такой записи аргументов (head, ...) нужно передать во вспомогательный макрос минимум два параметра. Поэтому макрос HEAD раскрывается в HEAD_HELPER(__VA_ARGS__, dummy). Второй аргумент dummy будет нужен, если у нас пустой список параметров, а нам нужно что-то отбросить. HEAD() раскроется в HEAD_HELPER(, dummy), который раскроется в первый переданный аргумент. Поскольку первый аргумент пуст, как нудистский пляж зимой, то раскрывается макрос в ничто. Второй аргумент dummy будет отброшен, он никогда не попадет в исходный код, в принципе, его имя можно вообще опустить.
Ничего сложного, в отличие от макроса REST. Вот тут придется подумать.
Не помню, кто впервые предложил такой путь решать эту задачу, но он явно продал душу Вельзевулу:
#define NUM(...) SELECT_10TH(__VA_ARGS__, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, dummy)
#define SELECT_10TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, ...) a10

#define REST_HELPER_ONE(first)
#define REST_HELPER_TWOORMORE(first, ...) , __VA_ARGS__

#define REST_HELPER2(qty, ...) REST_HELPER_##qty(__VA_ARGS__)
#define REST_HELPER(qty, ...) REST_HELPER2(qty, __VA_ARGS__)
#define REST(...) REST_HELPER(NUM(__VA_ARGS__), __VA_ARGS__)
Что здесь происходит? Макрос REST разворачивается в REST_HELPER, в качестве аргументов вызывается макрос NUM и пробрасывается оригинальный список параметров.
Название макроса NUM намекает, что размер имеет значение. Если аргументов больше одного, то макрос разворачивается в TWOORMORE, а если аргумент единственный, то в ONE. Больше десяти аргументов использовать нельзя, сломается, потому как в основе лежит трюк с SELECT_10TH. Этот макрос тупо выделяет десятый аргумент из переданного списка. Если передать пустой список или список с единственным параметром, то десятым аргументом будет ONE. Если __VA_ARGS__ содержит хотя бы два параметра, то список сдвигается и десятым аргументом будет уже TWOORMORE.
Далее REST_HELPER вызывает макрос REST_HELPER2, который формирует следующий вызываемый макрос: присоединяет к REST_HELPER_ окончание ONE или TWOORMORE (в зависимости от размера списка параметров).
Макросы REST_HELPER_ONE и REST_HELPER_TWOORMORE уже заготовлены. Обратите внимание, последний раскрывается вместе с запятой ", __VA_ARGS__", поэтому все работает идеально. Это будто некое завораживающее и одновременно отвратительное действо, от которого сложно оторвать взгляд. Вроде собеседования в Йандексе.
👍4🔥2😱2
Boost.Preprocessor
Многие ли из вас задумывались, какую мощь таит в себе обыкновенный препроцессор? Надеюсь, что нет, и вы разработчики без ментальных проблем. Однако кое-кто озадачился, поднатужился и выпустил целую библиотеку boost preprocessor со своими извращенными фантазиями. Так что появилось еще несколько оригинальных способов изготовить макрос для отладочной печати.
Включаем boost в проект, добавляем необходимые заголовочные файлы:
#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/comparison/greater.hpp>
И приготовим два макроса PRINT_0 и PRINT_1.
#define PRINT_0(fmt) printf("[%s:%d]" fmt "\n", __FILE__, __LINE__)
#define PRINT_1(fmt, ...) printf("[%s:%d]" fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
Первый пригодится для пустого списка параметров, где передается только строка форматирования.
Второй макрос будет весьма полезен во всех других случаях, когда в списке параметров отыщется хоть какой-нибудь захудалый аргументишка.
Теперь настало время реализовать сам макрос отладочной печати. Есть много хитрых методов, но мы остановимся на нескольких наиболее очевидных.
#define PRINT(...) \
BOOST_PP_CAT(PRINT_, BOOST_PP_GREATER( \
BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1))(__VA_ARGS__)
За основу тут взят макрос BOOST_PP_CAT. Из названия очевидно, что он соединяет свои два аргумента в единое существо. Как раз тут мы соединяем PRINT_ с результатом расширения макроса BOOST_PP_GREATER: нулем или единицей. Макрос BOOST_PP_GREATER просто сравнивает два числа, в данном случае единицу и результат BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), который вернет количество параметров в списке. В результате вызываться будет либо PRINT_0, если число параметров меньше единицы, то есть у нас есть только строка форматирования, либо PRINT_1 во всех других случаях.
Впрочем, это не единственный способ весело провести время. Также за основу можно взять макрос BOOST_PP_IIF, который расширяется вторым или третьим аргументом в зависимости от результата вычисления первого аргумента.
#define PRINT(...) BOOST_PP_IIF( \
BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), \
PRINT_1, PRINT_0)(__VA_ARGS__)
Тут все очевидно, то же самое выражение используется как условие: проверяем, больше ли единицы размер списка параметров, если больше, то вызываем PRINT_1, и PRINT_0 в остальных случаях. В принципе, оба этих варианта работают удовлетворительно, но для эстетов есть кое-что особенное.
#define PRINT(...) printf( "[%s:%d]"
BOOST_PP_VARIADIC_ELEM(0, __VA_ARGS__) "\n", __FILE__, __LINE__ \
BOOST_PP_COMMA_IF( \
BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1)) \
BOOST_PP_ARRAY_ENUM(BOOST_PP_ARRAY_POP_FRONT( \
BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__))))
Здесь мы работаем напрямую с функцией printf. Макрос BOOST_PP_VARIADIC_ELEM извлекает из списка параметр по индексу. Здесь нас интересует нулевой параметр, то есть строка форматирования. Дальше, после __LINE__ стоит макрос BOOST_PP_COMMA_IF, который расширится в запятую, если его значение аргумента истинно. В качестве аргумента тут хорошо знакомая строчка, думаю, понятно почему. Если аргумент ложный, то макрос расширится в ничто.
После возможной запятой нужно передать хвост списка параметров. Делается это весьма пугающим способом: BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__) формирует из списка параметров специальный препроцессорный массив, BOOST_PP_ARRAY_POP_FRONT отрывает ему голову, BOOST_PP_ARRAY_ENUM забивает массив обратно в список параметров.
Это в каком-то смысле даже изящно. Только нельзя забывать про необходимые заголовочные файлы, их много:
#include <boost/preprocessor/comparison/greater.hpp>
#include <boost/preprocessor/array/enum.hpp>
#include <boost/preprocessor/variadic/to_array.hpp>
#include <boost/preprocessor/array/pop_front.hpp>
иначе окунетесь в настоящий ад малопонятных сообщений об ошибках. Это будет даже похуже, чем ругань компилятора на неправильные шаблоны.
👍5
Macro __VA_OPT__
Сегодня Эдик собрал нас поговорить о пользе митингов.
"Некоторые из вас указали в опросе на прошлом митинге, что митинги неэффективны. Кто и почему предал наши идеалы?" - сурово вопрошал менеджер. Эдик митинги очень любил и считал их настоящей серебряной пулей, незаслуженно недооцененной иными руководителями. Что-то непонятно? Назначь митинг! Нужно ревью кода? Назначь митинг! Требования? Митинг!
"Эдик, никого их техлидов невозможно поймать, они постоянной на совещаниях! Может, сократим количество созвонов?" - взмолились разработчики-ренегаты.
"Не ожидал от вас... но vox populi vox Dei. Ничего не остается, мы назначим еженедельный митинг, где будем обсуждать пути уменьшения количества митингов с одновременным увеличением качества митингов!"
По ту сторону экрана кто-то мгновенно поседел. Неудивительно, совещания питаются нашим временем.
Время идет, работа стоит.
Работа стоит, а язык уходит вперед, развивается. И вот стандарт C++20 приносит нам не только всякие концепты и корутины, но и кое-что для макросов с переменным числом параметров. Проклятый комитет! Мало им языка, они и до препроцессора добрались, ничего святого!
Когда мы уже мощно выдохнем свое негодование в адрес комитетчиков, спрессовав ее в простые и емкие слова обсценной лексики, то с удивлением узнаем, что появился новый макрос __VA_OPT__.
Идея созрела аж в 2016 году, когда некий молодой человек по имени Томас Кёппе внес на рассмотрение рабочей группы предложение со странным названием "Comma omission and comma deletion". Незамысловатое описание гласило: "Это предложение призвано упростить использование макросов с переменным числом аргументов в случае отсутствия аргументов, путем добавления нового специального функционального макроса __VA_OPT__". Чувствуете? Человек на тех же граблях страдал, что и мы!
Вот теперь он избавит нас от всех проблем. Не сразу конечно, а в двадцатом стандарте.
После долгих и продуктивных дискуссий группа пришла к консенсусу, решив, что наиболее простым, понятным и удобным будет следующий синтаксис:
новоиспеченный макрос может появляться только перед __VA_ARGS__, если аргументы есть, то вызов __VA_OPT__(content) заменяется его аргументом content, а ежели нет, то __VA_OPT__ заменяется на беспросветную пустоту.
Канонический пример использования - это всем уже полюбившаяся отладочная печать:
#define PRINT(msg, ...) printf("%s:%d: " format_string "\n", __FILE__, __LINE__ __VA_OPT__(,)__VA_ARGS__);

PRINT("hello world") // => printf("%s:%d: hello world", __FILE__, __LINE__)
PRINT("hello world", ) // => printf("%s:%d: hello world", __FILE__, __LINE__)
PRINT("hello %d", n) // => printf("%s:%d: hello %d", __FILE__, __LINE__, n)

Если список __VA_ARGS__ не пустой, то __VA_OPT__ разворачивается в запятую, а если нет, то и запятая не нужна. Впрочем, если хорошенько подумать, можно найти и нетривиальный способ применения чудо-макроса.
#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })
SDEF(foo); // -> S foo;
SDEF(bar, 1, 2); // -> S bar = { 1, 2 };

У первого вызова SDEF пустой список параметров, поэтому инициализацию дописывать не нужно, а у второго целых два параметра, которые можно записать в скобочки и выдать за инициализацию структуры.
Если вас волнует вопрос, будет ли такое в чистом Си, то в конце предложения этот вопрос поднимается в полный рост. Мол, если комитет одобрит, то мы такой же документ для незамутненного Си исполним, чтоб не потерять обратную совместимость в будущем. Похоже, не обманули, такой же макрос ожидается в C23.
Вот какие причудливые зигзаги порой история вычерчивает, будто неловкая рука подростка на капоте неправильно припаркованной БМВ.
👍8
Сирены обмана
Приятно работать, глядя на закат. Сидеть перед приоткрытым окном, медленно тянуть остывший сладкий кофе, лениво ковыряя глупые ошибки в коде.
Вдруг, подняв в очередной раз глаза от монитора, чтоб полюбоваться на раскрасневшееся и отяжелевшее солнце, я обнаружил перед собой голову. Явно женская, только вот крепилась к телу довольно крупной птицы. Я икнул от неожиданности, и выпитый кофе пошел носом.
"Добрый день! Я Сирена Ахелоевна, IT рекрутер и предлагаю все-таки опять рассмотреть вакансию компании Йандекс!" - выпалило существо заученный текст, пока жертва в моем лице не успела опомниться.
"Опять ты?" - я запустил в нее кружку, но промахнулся и угодил точно в идущего домой соседа-психоаналитика.
Впервые эта птица прилетела ко мне больше десяти лет назад. Сладкие речи, сияющий ореол флагмана отечественного IT и транснациональной нидерландской компании тут же возымели эффект. Я даже не думал сопротивляться, когда сирена унесла меня на собеседование.
На интервью усталый голос томящегося в застенках гребца велел мне вертеть строчку. Точно уже и не вспомню, что надо было с ней сделать. Это явно была какая-то очередная нудная задача из большого и пыльного мешка скучных задач для отпугивания кандидатов. Я добросовестно крутил проклятую строку, но все, чего я добился, это испарина и усталость. Тогда мне было невдомек, что надо все свободное время посвящать решению подобных этюдов, если хочешь работать в компании с мировым именем.
Без претензий, алгоритмы и структуры важны. Просто нам не по пути, интересы разные. Мне нравится мигать светодиодами, а этой компании нужно, чтоб ты с огоньком делал скучные задачи, желательно за еду.
Потом я постарел. С возрастом только волосы в ушах растут лучше, а елейные речи хитрой птицы-рекрутера уже хуже слышишь.
"В вашей багодельне нет embedded, и никогда не будет!" - достаточно было бросить ей в лицо, чтоб она обиделась и улетела, оставаясь на связи.
Громкие стоны подбитого соседа за окном вернули меня к суровой действительности.
Коварная сирена уже сидела на подоконнике и, обгадив карниз, пела: "Ты ошибся, теперь у нас есть embedded! Складские роботы, беспилотные технологии...".
Кажется, голос пернатой твари стал звучать как-то приятнее.
"Ну ладно, неси на собес", - я раскинул руки и замер.
"Давай лучше в зуме", - сирена скептически осмотрела мое раздобревшее тельце, - "завтра в три часа, поговорите немного о языке С++".
Как загипнотизированный, я добавил событие в календарь. Что я теряю? Говорить о языке интересно и полезно.
На следующий день к назначенному часу я переоделся в выходной костюм. Не знаю зачем, камера не работает с прошлого собеседования, когда я разбил ее точным ударом кочерги. Так я провалил стресс-тест, как оказалось.
Звонок начался, и я снова услышал знакомый усталый голос гребца. Он равнодушно велел мне вертеть дерево.
Кажется, надо было найти высоту дерева или длину, или что-то такое же бесконечно скучное из того же бесконечного мешка с бесконечно скучными задачами для мумификации соискателя.
Я старался изо всех сил, вертел дерево и так и сяк. Ради интересных вопросов о C++ можно потерпеть даже это.
"Хорошо, а теперь..." - тут голос начинает произносить условия второй задачи, но переходит на язык змей, или птиц, или... математиков.
"Отпусти меня, дяденька!" - падаю на колени и молитвенно складываю руки, - "я не хочу алгоритмическую секцию, я вообще о плюсах пришел говорить".
"Ты что, больной?" - озадачен голос, - "на интервью только об алгоритмах и говорят!".
"Но ведь проект... embedded...", - всхлипываю я.
"Вообще не важно какой проект, других собесов у нас для вас нет. Про плюсы иди и смотри доклады Антона".
В ужасе от такого подхода к набору разработчиков я позорно бежал с интервью. Не будем критиковать компанию, каждый имеет право искать сотрудников, как им вздумается. Однако птицу видно по полету, а компанию - по собесу. Нам не по пути, потому как если даже собесы вызывают отвращение, каково там работать? Можете не отвечать, все равно не услышу, залил уши воском. В третий раз сирена меня не одурачит.
😁9👍3🔥2👏1
std::is_trivial
Коллеги настойчиво просили пояснить за магию. Жили б мы в мрачном средневековье, они бы уже давно плотной толпой, снаряженной вилами и факелами, окружили мою землянку.
Когда все работает, то наш велосипед лучше, чем в стандартной библиотеке, а как только на горизонте замаячили странные ошибки компиляции, то: "Что же ты натворил! Никуда не годятся твои шаблоны". Хотя они такие же мои, как и их.
Хорошо, люди с мрачными лицами, что у вас там опять стряслось?
Угрюмые разработчики отвечали мне надутыми обидой губами, что класс, мол, пытается залезть туда, где его не ждут. Вообразите себе контейнер, что имеет частичную специализацию для тривиальных типов. Этих типов, очевидно, стоит обрабатывать отдельно. С ними можно не миндальничать, а переносить побайтовым копированием, что изрядно упрощает реализацию. Так вот, один наглый класс пытался пролезть именно в контейнер для тривиальных, хотя за версту видно, что он рожей не вышел.
struct nontrivial {
nontrivial() = delete;
nontrivial(const nontrivial&) = delete;
nontrivial(nontrivial&&) = delete;
nontrivial& operator=(const nontrivial&) = delete;
nontrivial& operator=(nontrivial&&) = delete;
~nontrivial() = default;
nontrivial(uint32_t const value) : value_{value} {}

uint32_t value_;
};

Интуиция старого и больного плюсовика говорит мне, что это непростой класс, однако метафункция std::is_trivial, используемая для сортировки классов, ничтоже сумняшеся определяет его как тривиальный. Нельзя сказать, что std::is_trivial непогрешим, можно найти много гневных жалоб на его странную работу в разных компиляторах. Возможно, потому, что определение тривиальности постоянно корректировалось, понимание термина слегка размыто как и реализация метафункции.
Что же такое тривиальность?
За 2012 год можно найти такое формальное определение:
Тривиальный класс - тривиально копируемый класс с тривиальным конструктором по умолчанию.

Шли годы, на дворе 2017, язык развивается, определение тоже:
Тривиальный класс - тривиально копируемый класс с одним или более конструктором по умолчанию, которые или тривиальны, или удалены, но хотя бы один не должен быть удален.

И вот в 2023 мы видим последнюю, надеюсь, формулировку:
Тривиальный класс - тривиально копируемый класс с одним или более приемлемым конструктором по умолчанию, каждый должен быть тривиальным.

Кстати, "приемлемый" (eligible) в данном случае значит, что он не должен быть удален! Также "приемлемость" означает, что ограничения должны быть удовлетворены и не должно быть таких же конструкторов с большими ограничениями, но эти возможности относятся уже к концептам двадцатого стандарта, здесь мы такое не используем.
Почему же приведенный выше класс нетривиальный?
Начнем с главного. Его создатель явно пытался лишить его всяких конструкторов по умолчанию. Во всех трех приведенных определениях именно конструктор по умолчанию фигурирует как основное условие тривиальности: должен быть хотя бы один. Иные конструкторы могут существовать параллельно, даже иметь пользовательскую реализацию, они на тривиальность не влияют.
К сожалению, некоторые коллеги до сих пор убеждены, что если конструктор по умолчанию нам не нужен, то самое верное - удалить его явно.
Отсюда и строчка с nontrivial() = delete;
Они настаивают, что эту мантру надо писать, даже если есть другой конструктор, который делает неявное создание конструктора по умолчанию невозможным. Перестраховщики.
Теперь это вышло им боком. Спасибо GCC.
Не хотелось бы думать, что он не видит разницы между nontrivial() = delete; и nontrivial() = default; но уж очень похоже. Удалите конструктор по умолчанию, std::is_trivial тут же признает класс тривиальным, хотя у нас были совершенно противоположные устремления.
static_assert(std::is_trivial<nontrivial>::value);  // OK for GCC!

Хорошо хоть, IAR не впадает в ересь и совершенно справедливо причисляет nontrivial к нетривиальным типам. Такая разница в поведении компилятором генерирует много головной и иной боли при работе с шаблонами.
Впрочем, это не единственные странности в работе std::is_trivial. A suivre.
👍6😱2
std::is_trivially_copyable
На митинге невыносимо скучно, владелец продукта с видом важного, выбившегося в цари человека несет какую-то пургу. У меня же за окном пурга несет какого-то несчастного вида человека. "Рубист, наверное", - подумал я, но тут же забыл о нем. Приглушил звук митинга, отключил камеру и сосредоточился на главном вопросе: "Что есть тривиальный класс?"
Тут GCC и IAR устроили саботаж, готовы чуть ли не любой класс признать тривиальным. Хотя на самом деле, такой класс должен, во-первых, быть создан оригинальным способом: тривиальным конструктором по умолчанию. Во-вторых, и это немаловажный признак, класс должен быть тривиально копируемым.
Возникает закономерный вопрос: "Что есть тривиально копируемый класс?".
Когда деревья были большими и красно-черными, году эдак в 2012, формулировка была удивительно простой:
Есть нет нетривиальных копирующих конструкторов, нет нетривиальных конструкторов перемещения, нет нетривиальный операторов присваивания копированием и перемещением, зато есть тривиальный деструктор, то класс тривиально копируемый.

Потом стандарт понесся по кочкам вдаль, появились всякие явные удаленные специальные члены класса, в общем, стало понятно, что систему надо менять. Хотя бы определение.
В одном из последних черновиков стандарта можно найти следующие признаки тривиально копируемого класса:
- каждый копирующий конструктор, конструктор перемещения, операторы присваивания копированием и перемещением либо удалены, либо тривиальны.
- есть хотя бы один неудаленный копирующий конструктор, конструктор перемещения, операторы присваивания копированием или перемещением.
- имеется тривиальный и неудаленный деструктор.

Я честно подсмотрел у clang, что на основе этого описания можно смастерить свою метафункцию is_trivially_copyable:
namespace my {
template <typename T>
struct is_trivially_copyable {
static constexpr bool value {
std::is_trivially_destructible_v<T> &&
(!std::is_copy_constructible_v<T> || std::is_trivially_copy_constructible_v<T>) &&
(!std::is_move_constructible_v<T> || std::is_trivially_move_constructible_v<T>) &&
(!std::is_copy_assignable_v<T> || std::is_trivially_copy_assignable_v<T>) &&
(!std::is_move_assignable_v<T> || std::is_trivially_move_assignable_v<T>) &&
(std::is_copy_constructible_v<T> || std::is_move_constructible_v<T>
|| std::is_copy_assignable_v<T> || std::is_move_assignable_v<T>)};
};
} // namespace my

А теперь сравним поведение my::is_trivially_copyable и std::is_trivially_copyable при проверке класса nontrivial из предыдущего поста. Напомню как он выглядел:
struct nontrivial {
...
nontrivial(const nontrivial&) = delete;
nontrivial(nontrivial&&) = delete;
nontrivial& operator=(const nontrivial&) = delete;
nontrivial& operator=(nontrivial&&) = delete;
...
};

Класс ожидаемо не проходит проверку самописной функцией:
static_assert(my::is_trivially_copyable<nontrivial>::value);  // FAIL

Это даже не тривиальный класс, все копирующие конструкторы удалены, его вообще нельзя копировать!
static_assert(std::is_trivial_copyable<nontrivial>::value);  // OK

А вот GCC и IAR считают по-другому. Копировать нельзя, но если тривиально, то можно. (К слову, std::is_trivial<nontrivial>::value == true, класс еще и тривиальный).
Что они имеют в виду? Что можно копировать их функцией memcpy без катастрофических последствий? Но копирующие конструкторы просто так не удаляют. Разработчик явно не хотел, чтоб класс можно было копировать каким-либо образом. Так или иначе, это нарушение стандарта. Народ не проведешь, они это замечают и заводят баги для компиляторов, например, №96288 для GCC, №39050 для LLVM.
Те, кто уже вкусил запретного плода познания, буквально кричат на специальных форумах:
Do not use is_trivial nor is_trivially_copyable! EVER!!!

Это все равно, что играть с огнем! Определение слишком большое, опирается на другие определения, которые в любой момент безумный комитет готов поменять. И вся ваша аккуратно и с большой любовью написанная библиотека развалится, как прогнивший люфт-клозет.
👍6🤔1
Как починить std::atomic
Человечество с давних времен беспокоило два вопроса: "Доколе уринотерапия будет на задворках медицины?" и "Можно ли копировать объект std::atomic?".
Некоторые авторитетные разработчики утверждают, что atomic не всегда использует lock-free механизм, поэтому в его внутренностях могут встречаться мьютексы, которые уж точно не подлежат размножению почкованием. Однако зачем вообще его копировать? Что есть копия объекта атомарного типа? Будет ли новый объект просто представителем оригинала? Нет четкого понимания, что должен делать атомарный тип при выполнении операции копирования. Поэтому копирующие конструкторы и были запрещены издревле.
Как мы уже знаем, удаление специальных членов класса изначально не являлось веской причиной терять тривиальность:
static_assert(std::is_trivially_copyable<std::atomic<int>>::value);  // OK

То есть, компилятор гарантирует, что std::atomic<int> можно копировать побайтно.
Тут даже самые стойкие разработчики могут поддаться соблазну применить memcpy. Причем вполне легально, что и было явной проблемой.
Йенс Маурер (Jens Maurer) пополняет список дефектов в 2014 году записью LWG2424 Should state that atomic types are not trivially copyable.
Начинаются жаркие дебаты о том, как обустроить atomic. Можно же исправить стандарт и включить туда запись о том, что этот тип не может быть тривиально копируемым? В сопутствующем документе (LWG 2424: Atomics, mutexes and condition variables should not be trivially copyable) Вилле Вотилайнена (Ville Voutilainen) в 2015 году были собраны иные извращения:
Предлагалось не удалять копирующий конструктор, а сделать его пользовательским и закрытым:
private:
atomic& operator=(const atomic&);

Все равно друзей у атомарного типа нет, никто не сможет добраться до этого конструктора.
Было предложение не мучиться и специализировать метафункции is_trivially_copyable и is_trivial, чтоб для типа atomic она всегда возвращала false.
Забавно, но такое решение было заклеймено как "злое". Странно, правда?
А закончилось все ничем. Изменился стандарт, формально std::atomic перестали быть тривиальными, хоть компиляторы с этим не согласились.
Вот сейчас, исследуя, что принес нам c++20, еще раз запускаем проверку:
static_assert(std::is_trivially_copyable<std::atmoic<int>>::value);  // FAIL

Чудо! std::atomic перестал быть тривиальным. Нет, компилятор по-прежнему не питает уважения к удаленным специальным методам.
Но оказалось, что положение спасла другая проблема std::atomic, описанная в 2019 году Николаем Йосуттисом (N. Josuttis, P0883R2: Fixing Atomic Initialization)
Если вы еще не заметили, хотя это лежит на поверхности, инициализация объекта очень странная.
std::atomic<int> x{};

Нет гарантии, что значение переменной внутри x будет нулевым, хотя это стандартная нулевая инициализация.
Для того, чтоб иметь тривиальный конструктор, std::atomic не делает инициализацию внутреннего объекта. Такое поведение гарантирует совместимость с Си.
"Это даже хуже, чем UB; Мы требуем не делать правильного и ожидаемого действия!" - в ужасе восклицает Николай.
"Такое поведение, когда невозможно инициировать std::atomic<T> значением по умолчанию (T{}), не что иное, как источник багов и удивления", - вторит ему Герб Саттер.
Черт с этой совместимостью, никто о ней и не вспомнит, срочно резать не дожидаясь перитонита!
Исправленный базовый класс atomic выглядит примерно так:
template<typename _ITp>
struct __atomic_base {
alignas(_S_alignment) __int_type _M_i _GLIBCXX20_INIT(0);

где _GLIBCXX20_INIT это простейший макрос:
#if __glibcxx_atomic_value_initialization
# define _GLIBCXX20_INIT(I) = I
#else
# define _GLIBCXX20_INIT(I)
#endif

По счастливой случайности, приобретя нетривиальный конструктор, атомарные типы гарантированно лишились тривиальности.
🤯5👍3😱1
std::underlying_type
Почему-то некоторые коллеги, из тех кто отмечает католическое рождество, считают хорошим тоном выключить на праздники всю тестовую инфраструктуру. Вероятно, считают, что покрасневшие шарики билдов радуют глаз, поднимают настроение... и артериальное давление.
Отчасти поэтому я взорвался негодованием, когда увидел "матафункцию", изготовленную одним из таких "товарищей":
template <class T>
constexpr bool IsIntegralEnum() {
bool to_return {false};
if constexpr (std::is_enum<T>::value) {
if constexpr (std::is_integral<typename std::underlying_type<T>::type>::value) {
to_return = true;
}
}
return to_return;
}

Вот это вот смотрело на меня из глубин кода. Функция должна подтвердить, что тип T - это перечисление с базовым целочисленным типом. Вопрос напрашивается сам собой: "Ты здоров? На каком еще типе может быть основано перечисление?!"
Тут же открываем стандарт, находим раздел о перечислениях: 9.7.1 Enumeration declarations [dcl.enum].
Здесь совершенно ясно написано, как перечисление может выглядеть:
enum-key:
...
enum-base:
: type-specifier-seq

Имя type-specifier-seq секции enum-base должно указывать на целочисленный тип; любые cv-квалификаторы игнорируются.
То есть базовый тип перечисления всегда целочисленный, нет смысла проверять это!
Даже если очень захотеть, то иной тип и не запихнешь:
enum class Weird : float {};  // FAIL
error: underlying type 'float' of 'Weird' must be an integral type

Теперь пройдемся по форме метафункции. Да, она помечена как constexpr, но никто не обязывает выполнять ее исключительно во время компиляции.
Для static_assert сойдет, но в иных контекстах есть вероятность того, что функция будет создана и вызвана в runtime:
_Z14IsIntegralEnumIiEbv:
push {r7}
sub sp, sp, #12
...

Еще существует опасность неправильного использования при проверке: static_assert(IsIntegralEnum<int>);
Можно случайно забыть круглые скобочки. Самое страшное, что это не будет ошибкой, и проверка будет проходить всегда.
Если уж и проверять базовый тип перечисления, я бы предпочел другую форму:
template <class T, class = void>
struct IsEnumBool : std::false_type {};
template <class T>
struct IsEnumBool<T, std::void_t<typename std::underlying_type<T>::type>> {
static constexpr bool value {std::is_same<typename std::underlying_type<T>::type, bool>::value};
};

Допустим, мы хотим проверить, что базовый тип bool. Просто создадим частичную специализацию для случая когда std::underlying_type<T>::type имеет смысл. Как известно, результат этой метафункции будет существовать только для перечислений. То есть std::is_enum выполняется автоматически. А дальше просто накладываем нужное условие на базовый тип.
Моему "коллеге" счастливого рождества, и пусть горит в аду. Всех остальных с неумолимо наступающими праздниками! 🥳
👍5
This media is not supported in your browser
VIEW IN TELEGRAM
С новым годом!
Спокойно, работа все еще не волк, праздники продолжаются, салаты доедаются, веселье бьет как кастетом по головам. Танцуем вместе с платами! 🥳
🔥7🤔1🤯1
prospective destructor
Думаю у каждого, кто изобретал свой optional в новогодние праздники, было дикое желание реализовать в классе разные деструкторы: один для тривиальных типов, и второй для всех остальных. Однако деструктор - плохой метод класса, он, будто ветеран холивара, не принимает никакие аргументы и ничего не возвращает, как сбербанк советские вклады. Поэтому никакие перегрузки и SFINAE его не возьмут.
А хорошо бы иметь разные деструкторы:
template <class T>
MyOptional {
...
~MyOptional() = default;
~MyOptional() { // for non-trivial T
value_.~T();
}
...
T value_;
};

В этом случае класс MyOptional сохранил бы свойство тривиальности типа T.
static_assert(std::is_trivial_v<MyOptional<trivial>>);
static_assert(!std::is_trivial_v<MyOptional<nontrivial>>);

Проблема в том, что у класса может быть только один деструктор. И вот, в стандарте c++20 появляется очень простое и изящное решение проблемы: предполагаемые деструкторы.
Да, название так себе. Лучше назовем их призрачными деструкторами. Они вроде как есть, но в то же время и нет. При каждом инстанцировании шаблона только один деструктор должен быть активен. Для этого достаточно указать требования к каждому кандидату, например:
~MyOptional() = default;
~MyOptional() requires(!std::is_trivial_v<T>) {
value_.~T();
}

Тогда, если T - это нетривиальный тип, деструктор по умолчанию игнорируется, и создается тот, что больше подходит под требования.
Слишком просто. Нужно немного усложнить...
👍3🤔1
prospective destructors
Условия могут быть и более интересными. Например, если мы работаем с очень специфичными объектами, которые нужно уничтожить, предварительно вызвав метод destroy:
~MyOptional() requires requires(T c) { c.destroy(); } {
value_.destroy();
value_.~T();
}

Впрочем, здесь уже может возникнуть конфликт. Если тип нетривиальный и имеет в составе метод destroy(), появляется неоднозначность: аж целых два деструктора подходят под требования. Закономерная ошибка компиляции. Исправить ситуацию можно как и в SFINAE, просто исключив нужное подмножество из требований.
~MyOptional() requires requires(T c) { c.destroy(); } {
value_.destroy();
value_.~T();
}
~MyOptional() requires(!std::is_trivial_v<T>) && (!requires(T c) { c.destroy(); }) {
value_.~T();
}

Кажется, что это уж слишком и требования режут глаз, как картины Френсиса Бэкона? Соглашусь, для более понятного кода можно определить сначала концепты.
template <class C>
concept NonTrivial = !std::is_trivial_v<C>;

template <class C>
concept Destroyable = requires (C c) {
c.destroy();
};

А уж потом смело манипулировать ими самым неприглядным, но понятным способом:
~Some() requires NonTrivial<T> && (!Destroyable<T>) {...}
~Some() requires Destroyable<T> {...}

О дивный новый стандарт! Впрочем, не такой и новый, это просто IAR его не понимает.
👍2
pseudo prospective destructor
Не всем повезло использовать новейшие стандарты в работе. Или же протащить их через цензоров. Кто-то застрял в старых стандартах, как в текстурах. Впрочем, как и я. Стандарты хоть и старые, но никем не отмененные, а настоящий индеец сможет реализовать эти ваши призрачные деструкторы и без новых трюков.
Допустим, нем нужно решить такую же задачу: MyOptional класс должен быть тривиальным, если содержит тривиальный же тип.
Как же сохранить свойство тривиальности исходного типа, если с деструктором класса мы ничего сделать не можем? То же, что делает любая управляющая компания: переложим ответственность на подрядчика. В смысле, созданием и уничтожением объекта будет заведовать некий базовый класс.
template <class T>
struct MyOptional : private Base<T> {
MyOptional() = default;
~MyOptional() = default;
...
};

Для реализации разного поведения для тривиальных типов и остальных используем частичную специализацию шаблона:
template <class T, bool = std::is_trivial_v<T>>
struct Base {
~Base() = default;
T value_;
};

template <class T>
struct Base<T, false> {
~Base() { value_.~T(); };
T value_ {};
};

Выглядит не очень изящно, но это достаточно условный код. Его цель - показать, как реализовать разное "обслуживание" разных типов.
Здесь используется прием, отдаленно напоминающий трюк с void_t, только наоборот. Второй параметр шаблона - переменная типа bool. Ее можно указать явно при использовании шаблона, а можно положиться на значение по умолчанию, что мы и делаем. По умолчанию этот параметр вычисляется метафункцией std::is_trivial_v<T>, и если она равна false, т.е. тип нетривиальный, компилятор радостно подхватит вторую более конкретную частичную специализацию Base<T, false>. Если же результат будет равен true, то ничего не остается, как остановиться на более общей первой специализации.
Вторая нетривиальная специализация обработает нетривиальный тип как следует и попутно лишит тривиальности и наследника MyOptional.
Беспокоиться о наследовании здесь не стоит. Тут не про интерфейс и реализации, а про то, как прицепить к классу нужные детали реализации для разных типов. Тем более, что наследование приватное, а значит, приведение к такому базовому типу будет невозможно.
Base<int> *p {new MyOptional<int>{}};
error: Base<int, true>' is an inaccessible base of 'MyOptional<int>'

Способ не единственный, но в стандартной библиотеке частенько используется именно он.
👍2🔥1🤔1
reverse
"В плюсы веруете?" - бодро спрашивала Ксения, рекрутер одной крупной международной компании.
"Верую!" - робко отвечал я, боясь спугнуть удачу.
"И в двадцатый стандарт веруешь?"
"Верую!"
"А стандартную библиотеку используешь?"
"Использую!"
Собеседований у меня давно не было, и организм требовал новых интересных вопросов, задачек и прочих вызовов. Игривый тон HR-специалиста внушал скромный оптимизм.
"А питон у вас как?" - спросила напоследок Ксения, совсем потеряв стыд.
"Не жалуюсь, мой питон еще ни разу не подвел, - ответил я, смутившись. - А надо будет предъявить?"
"На техническом собеседовании могут проверить".
Но на следующем этапе мне повезло. Никто не просил непотребства или Rust-a, и я успешно миновал все душные вопросы, единственной целью которых, думаю, было утомить и усыпить бдительность соискателя. Однако не могу сказать, что задача про реверс битов в целочисленной переменной застала меня врасплох. Я ждал подобной подлости.
Терпеть не могу лайфкодинг. Он похож на общественный туалет, где отсутствуют не только двери кабинок, но и перегородки между белыми друзьями. Я нехотя открыл знакомый всем интерактивный онлайн-компилятор, где резво записал тестовый пример:
int main() {
uint32_t x {0x00000001U};
return reverse(x);
}

Мой собеседник одобрительно икнул. Чуть выше я стал писать реализацию функции reverse.
Записал первую строчку:
uint32_t reverse(uint32_t const x) {

и задумался на минуту. Что дальше? Пошлый цикл? Это мещанство! Было же у этой задачи какое-то короткое решение? Надо сразить наповал, решился я.
Быстро, пока никто не успел опомниться, записал тело функции:
  return Rev(std::make_index_sequence<sizeof(x) * 8U>{}, x);
}

Кажется, с противоположного конца стола послышался сдавленный вскрик.
Как можно быстрее, словно опасаясь удара в спину, я добил последние строчки:
template <size_t Index>
uint32_t RevBit(uint32_t const x) {
return (x & (1U << Index)) ? (1U << (31U - Index)) : 0U;
}
template <size_t ... Ixs>
static uint32_t Rev(std::index_sequence<Ixs...>, uint32_t const x) {
return (RevBit<Ixs>(x) | ...);
}

Все же понятно? Если по определенному индексу Index у нас есть бит, мы выставляем бит по индексу 31U - Index, то есть как бы зеркально отражаем его. Ну и немного шаблонов для веселья.
Godbolt предательски выдал: "Program returned: 0". Тестовый пример не сработал. Быть такого не может! Программа должна была вернуть 0x80000000! Наверное, мой собеседник изрядно повеселился, любуясь моими потугами найти несуществующую ошибку в коде. Хотя нужно было лишь вспомнить, как работает return в POSIX системах. Значение имеют только последние 8 бит, кроме них, вы ничего не сможете передать в родительский процесс.
Уставший и опозоренный, я вернулся домой доедать голубцы со странной начинкой и привкусом поражения.
👍7👏2
IAR наносит ответный удар.
Мой послеобеденный сон прервал Федя. Мгновенно распрощавшись с хорошим настроением, я стукнул по кнопке вызова. На его звонки всегда отвечаешь с опаской, поскольку он имеет славу неуемного рационализатора и кодификатора, что неустанно улучшает процесс разработки. Правда, в итоге мы всегда вязли в его выдумках, а наши бедные пулл-реквесты застревали в привнесенных иезуитских практиках.
- Здравствуй, дорогой друг! - просиял Федя.
- Рад слышать тебя, - тоже соврал я.
- У нас проблемы. Новый IAR 9.50 отказывается собирать нашу прошивку!
Я закатил глаза, давая понять, насколько опечален. Впрочем, спросонья никак не мог взять в толк, зачем нам новый компилятор? Старый еще не кончился.
Оказалось, что в новой версии есть какая-то умопомрачительная особенность, которая никому не нужна, кроме Феди, а значит, нужна всем.
Тут же уважаемый коллега показал мне проблемное место в коде.
Допустим, у нас есть некая структура Resource, состоящая из имени и двух нестатических членов нетривиального типа:
struct Resource final {
std::string_view name_;
nontrivial setter_ {};
nontrivial getter_ {};
};

Теперь создадим объект структуры:
Resource x {
.setter = nontrivial{0x01U}
};

Здесь новая версия компилятора начинает жаловаться и стенать:
Error [Pe1563]: class type not suitable for use with designators

Опять designators! То есть назначенные инициализаторы! А что ты хотел, братец?! Они же все закона до с++20. Плачьте, любители незамутненного Си, провидение карает вас за стандартоотступничество. Хоть это и выглядит как баг. IAR не жаловался на подобные конструкции ранее, если включены расширения опцией -e. Конечно, никто не гарантировал, что список расширений будет неизменен, но мы ожидаем именно этого от хорошего компилятора. Обратной совместимости.
В поддержке IAR вроде как согласились, что поведение странное, возможно, из-за нетривиальных классов в составе структуры, но это не точно. Дали бесплатный совет, как побороть сей недуг: при сборке использовать опцию
--warn_about_missing_field_initializers

Опция заставляет компилятор выдавать предупреждение, если не у всех членов структуры есть явный инициализатор.
В новой версии разработчики решили еще более приблизиться к с++20 в том числе и в вопросе назначенных инициализаторов. В стандарте было простое требование, что все назначенные инициализаторы используемые в выражении должны появляться в том же порядке, что и в структуре. Тут же ребята перестарались. По умолчанию, видимо, там, где warn_about_missing_field_initializers выдал бы предупреждение, компилятор выдает ошибку. Соответственно, этой опцией мы немного снижаем накал страстей, превращаем ошибки в предупреждения. Таким вот оригинальным способом можно обойти богомерзкий баг.
Я с грустью смотрел, как полчища сообщений "Warning" заполняют экран при компиляции. Федор же радовался, как ребенок, он знал хорошенький способ подавления предупреждений...
👍4🤔2
reverse 2
Многие спрашивают, чем же закончились мои отношения с крупной международной компанией "Ой-Вей". Ладно, никто не спрашивал, но присочинить очень хочется.
Начнем с того, что у задачи про разворот битов в uint32_t есть классическое, то есть страшное и производительное, решение.
uint32_t reverse2(uint32_t x) {
x = (x & 0x55555555U) << 1U | (x >> 1U) & 0x55555555U;
x = (x & 0x33333333U) << 2U | (x >> 2U) & 0x33333333U;
x = (x & 0x0F0F0F0FU) << 4U | (x >> 4U) & 0x0F0F0F0FU;
x = (x << 24U) | ((x & 0xFF00U) << 8U) | ((x >> 8U) & 0xFF00U) | (x >> 24U);
return x;
}

На первый взгляд некрасиво, на второй безобразно, но не будем торопиться с выводами, а хладнокровно разберемся, как это чудо работает.
Допустим, есть у нас байт, пронумеруем его биты: 7 6 5 4 3 2 1 0
Маска 0x55 (0b01010101) выделяет четные биты. Если сдвинуть байт на один бит вправо, то масочка четко выявит нечетные биты. Дальше, я полагаю, все очевидно. В первом выражении мы меняем местами четные и нечетные биты:
6 _ 4 _ 2 _ 0 _ | _ 7 _ 5 _ 3 _ 1 --> 6 7 4 5 2 3 0 1
Затем сдвигами и масками меняем местами пары бит:
4 5 _ _ 0 1 _ _ | _ _ 6 7 _ _ 2 3 --> 4 5 6 7 0 1 2 3
Маскарад продолжается, меняем местами четверки:
0 1 2 3 _ _ _ _ | _ _ _ _ 4 5 6 7 --> 0 1 2 3 4 5 6 7
В результате этого фокуса биты стоят в обратном порядке. Последнее выражение меняет порядок байтов в uint32_t.
Несмотря на нелепый вид этой функции, бенчмарки показывают, что она быстрее моей красивой шаблонной в 2 раза при максимальной оптимизации и в 8 раз резвее без оптимизаций.
Не знаю, как дойти до этого на собесе, где над тобой летают стервятники-интервьюеры, и нет ни малейшей возможности сосредоточиться на задаче.
Впрочем, есть мнение, что на интервью достаточно не пускать слюни и не ходить под себя, чтоб произвести приятное впечатление. Наверное, этой позиции и придерживаются в крупной межгалактической компании, а может, просто у них модная политика разнообразия требует набирать какой-то процент глупых разработчиков вроде меня. В любом случае они пригласили меня снова поговорить более предметно о вакансии. Более предметно - это про шекели, конечно.
Так вот, сидим мы со специалистом по человеческим ресурсам, кофий пьем, о погоде беседуем, как вдруг он выдает с ленинским прищуром: "А принесите мне справку 2-НДФЛ, чтоб подтвердить уровень дохода на прежнем месте. Таки наши зарубежные коллеги могут не понять, зачем вы столько просите...".
У меня от такой бестактности аж кофе носом пошел. Но отвлечь HR не получилось, он уже вцепился в рукав: "Прямо сейчас заказывай на госуслугах, а то в Яндекс отправлю на опыты!".
Страшная угроза заставила меня соображать быстрее.
"Знайте же, sizeof в плюсах не может вернуть ноль!" - соврал я и покраснел.
От неожиданности мой угнетатель чуть ослабил хватку, и я смог беспрепятственно провалиться от стыда сквозь землю. Плюхнувшись точнехонько на платформу станции метро, я поднялся, отряхнулся, дождался поезда и уехал домой.
👍9🤔1
sizeof
В прошлый раз я случайно затронул дискуссионный вопрос о том, может ли sizeof вернуть ноль. Если опираться только на ресурс cppreference, то ответ очевиден:
Результат выполнения оператора sizeof всегда ненулевой, даже если он применен к типу пустого класса.

Как говорил Страуструп, размер пустого класса должен быть больше нуля для пущей уверенности, что у двух разных объектов будут разные адреса, пароли, явки.
  struct Empty {};
Empty a, b;
assert(&a != &b); // OK

А что стандарт? В разделе 7.6.2.5 Sizeof говорится, что оператор sizeof возвращает количество байт, которое потребуется для размещения самостоятельного объекта, переданного в качестве аргумента типа. Это мы и так знали и понимали... на животном уровне. Есть ли в стандарте что-нибудь о минимальном размере класса? Ответ положительный, есть намеки.
В разделе 6.7.2 Object model можно найти упоминание, что объект не грудь и может иметь нулевой размер. Впрочем, надо соблюсти приличия: объект использовать только как составную часть (как базовый класс или пустой член класса с атрибутом [[no_unique_address]]), отказаться от виртуальных функций и базовых классов, убрать члены ненулевого размера, что понятно. В этом случае объект является подобъектом базового класса стандартной компоновки без нестатических членов и таки не стоит нам ничего. Однако понять это можно только по косвенным признакам, sizeof будет молчать об этом.
Хорошо нам знакомая оптимизация EBO (Empty base optimization):
struct A : Empty { int x; };
sizeof(A::Empty) == 1

Хорошо нам знакомый атрибут [[no_unique_address]]:
struct B {
[[no_unique_address]] Empty e;
int x;
};
sizeof(B::e) == 1

Следом идет приписка, что обстоятельства, при которых объект имеет нулевой размер, определяются реализацией. Какие еще обстоятельства могут возникнуть? Может быть, массив? Да не этот ваш std::array, а олдскульный, сишный!
Сколько потребуется байт, чтоб разместить массив нулевой длины? Интуитивно понятно, что в памяти не потребуется места вовсе. GCC подтверждает подтверждает нашу догадку:
static_assert(sizeof(int[0]) == 0);  // OK

Того же размера будет и класс, содержащий такой массив.
struct SuperEmpty { int x[0]; };

Теперь, если еще раз попытаться пройти тест Страуструпа, мы его успешно провалим:
  SuperEmpty a, b;
assert(&a != &b); // fail

Объекты имеют одинаковый адрес! Видите, какая опасная штука получилась?!
Хорошо, что этот трюк не совсем законный. В разделе 9.3.4.5 Arrays стандарта четко написано: размер массива N должен быть больше нуля. Если включить опцию -pedantic, то компилятор прозреет.
warning: ISO C++ forbids zero-size array [-Wpedantic]

Для IAR все намного проще, он костьми ляжет, но не даст вам сделать подобный объект. Даже sizeof откажется его считать:
Error[Pe094]: the size of an array must be greater than zero

Однако в некоторых случаях это допустимо. A suivre.
👍81
Cherchez la FAM
Нужно ли останавливать коллег, если они приносят вам код с грязными хаками или трюками? Конечно! Как писал Хармс: "Когда я вижу разработчика, мне хочется ударить его по рукам. Так приятно бить по рукам программиста!" Точно так и писал, а быть может, и я неправильно запомнил. В любом случае не рассказывайте сослуживцам содержание этого поста.
Так вот, в руководстве заклинателя IAR есть глава про расширения языка "C++ language extensions", что неудивительно, людей хлебом не корми, дай какой-нибудь стандарт нарушить, на газоне припарковаться или тушенку не по ГОСТу закатать. Одно из нарушений-исключений описывается следующим образом:
Последним членом структуры можно определить безразмерный массив или массив нулевого размера.

И даже приведены примеры массива нулевого размера:
typedef struct {
size_t size;
uint8_t payload[0];
} Header;

и безразмерного массива:
typedef struct {
size_t size;
uint8_t payload[];
} Header;

По сути для IAR разницы нет. Компилятор видит их как массив неполного типа, поэтому и точный размер назвать затрудняется. Однако позволяет таким объектам существовать.
Этот прием пришел из глубины веков, когда древние программисты возжелали некоторой свободы, корчась в ежовых рукавицах строгой типизации. Продав душу демону максвелла, придумали что-то вроде предка вектора. Типичный пример использования этого подхода:
Header *p = malloc(sizeof (struct Header) + 100);

Для структуры Header мы выделяем памяти немного или много больше, чем требуется. В результате мы можем оперировать выделенной памятью через член-псевдомассив payload, практически не опасаясь UB. Мы будто бы определили новый тип
struct { size_t size; uint8_t payload[100]; } *p;

но в том и прелесть, что тип тот же самый, а размер массива разный.
Для плюсов это действительно расширение, а вот в православной сишечке это вошло в стандарт C99.
В параграфе 6.7.2.1 Structure and union specifiers, стих 16:
Как особый случай последний элемент структуры с более чем одним именованным членом может иметь неполный тип массива, это называется гибкий массивный член. В смысле, это гуттаперчевый массив - член структуры. Лучше будем называть его flexible array member, FAM.
Морально готовьтесь к тому, что GCC будет презрительно морщиться, видя в коде нулевой массив:
warning: ISO C++ forbids zero-size array 'payload' [-Wpedantic]

и кривиться, видя FAM:
warning: ISO C++ forbids flexible array member 'payload' [-Wpedantic]

Потому что это не стандарт С++!
Нельзя сказать, что попыток убрать недопонимание с C99 не было. Существует рационализаторское предложение за номером P1039R0 от JeanHeyd Meneide, Nicole Mazzuca и Arvid Gerstmann.
Эти трое в лодке, не считая затраты, написали внушительную работу I got you, FAM: Flexible Array Members for C++ в далеком ныне 2018 году.
Прошло уже 6 лет, но каких-то сдвигов в этом направлении я не вижу. Да и не жду уже. Зачем? Расширения не вода, на лето не отключат.
👍6🔥1