C++ Embedded
424 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
printf & IAR
Какую первую программу напишет начинающий, но нормальный разработчик? Конечно, помигает светодиодом! Некоторые ошибочно полагают, что нужно писать что-то в духе printf("Hello, world!\r\n"), но поди тут объясни ребенку, куда процессор эту надпись должен совать, когда нет ни монитора, ни даже жалкого семисегментного индикатора.
- Батя, ты что?! В отладчике же есть терминал ввода/вывода...
- Молчи и продолжай моргать светодиодом!
Правда, во многих отладчиках такая возможность предусмотрена, что говорит о значительном количестве любителей и профессионалов отладочной печати. Почему нет, это самый эффективный метод нахождения глупых ошибок. Только вот под лежачий камень слеза не течет, придется приложить усилия, чтоб добиться приемлемого результата.
Во-первых, хорошо бы подключить соответствующий заголовочный файл <cstdio>.
Во-вторых, IAR по умолчанию не знает что с вашими письменами делать:
Error[Li005]: no definition for "__write" [referenced from putchar.o(dl7M_tln.a)]
Собственно, это дает нам пространство для маневра. Мы можем переопределить функцию __write(int, const unsigned char *, size_t), где опишем, как мы выводим символы. Например, захотим и через uart перенаправим, захотим - и послушная машина вымолвит сообщение голосом Левитана. Только не желаем мы сложностей, нам бы просто в отладчике подсмотреть что, где и как выводится.
Здесь на помощь бежит наш старый знакомый semihosting. Так и пропишем новый флаг для компоновщика: --semihosting. После этого вопросов у компилятора не возникнет, кое-какую прошивку с отладочным выводом уже можно собрать.
Что же произойдет, когда будет вызвана вожделенная функция printf? Взглянем на стек вызовов
main()
[printf + ...]
[_PrintfFullNoMb + ...]
[_Prout + ...]
[putchar + ...]
[__write + ...]
[__dwrite + ...]
[__iar_sh_stdout + ...]
Изначально вызывается так называемый formatter, начинающийся с _Printf. Эта функция знает как обработать аргументы, как перевести их в строку и что вызвать потом. По умолчанию IAR поставит самую жирную функцию, что есть в наличии, с полной поддержкой всевозможных вариантов аргументов.
Далее, получившую строчку посимвольно пропустят через putchar, которая, опять же, вызывает __write для каждого. Не слишком быстро, зато надежно.
Да и с функцией __write можно не мудрить, а предположить, что в нее не передадут никогда больше одной буковки.
Закончатся мытарства одинокого символа в функции __iar_sh_stdout, которая наконец-то переправит его на терминал. Среди прочего хлама здесь есть интересные инструкции:
 MOV       R1, SP
MOVS R0, #5
BKPT #0xab

Это и есть классическая обращение к хосту. В регистре R1 записан адрес памяти, где лежит подготовленный фрейм для отладчика, а в регистре R0 - номер команды. Что это за команда, могут пояснить исходники в IAR:
#define SEMIHOSTING_SYS_WRITEC           (0x03)
#define SEMIHOSTING_SYS_WRITE0 (0x04)
#define SEMIHOSTING_SYS_WRITE (0x05)
Здравый смысл говорит, что первая команда используется для передачи символа, вторая команда способна передать уже нуль-терминированную строку, а третью используем мы по умолчанию.
Если взглянуть на участок памяти, куда указывает регистр R1, то мы увидим нечто подобное: 01 00 00 00 d0 01 02 20 01 00 00 00.
Тут записаны три параметра, которые надо переправить на ту сторону. Первый - это номер потока (stderr или stdout), второй - указатель на передаваемую строку (здесь: 0x200201d0), третий - длина строки (единица, конечно же).
Интересно, в данном случае хватило бы и команды №3, но компилятор предпочел иной вариант. Видимо, чувствует негодяй, что мы не остановимся на достигнутом. A suivre.
👍6🤔1
__write_buffered & IAR
Неспешно прогуливаясь между красиво структурированными кучами кода, я вдруг заметил подозрительную функцию, которая бесстыдно выставила на показ все свое ассемблерное нутро.
void DebugPrint(char* data, uint32_t length) {
constexpr uint32_t kSemihostingSysWrite {5U};
constexpr uint32_t kSemihostingStdout {1U};
Frame frame {kSemihostingStdout, data, length};
asm volatile("mov r0, %0 \n" // command
"mov r1, %1 \n" // frame
"bkpt 0xab \n" : : "r"(kSemihostingSysWrite), "r"(&frame) :);
}
- Так, а кто это сделал? Что это такое вообще? - грозно вопрошаю я у бессловесного монитора.
Лежавший на подоконнике кот дернул ухом и лениво перевалился на другой бок, явно давая понять, что он тут ни при чем. Пришлось продублировать запрос в рабочем чате.
"Так ведь работает! Чего же тебе еще надобно, собака?" - шутливо отмахиваются от меня явно уязвленные нездоровым любопытством разработчики.
Сложно спорить, это должно работать, это же использование механизма semihosting, только вручную. Лично проверил опции линкера, --semihosting в списке не значится. Однако работает так же, как и вызов printf: в регистр R0 записывается команда SEMIHOSTING_SYS_WRITE, т.е. число 5, а в регистр R1 указатель на фрейм с данными, которые надо передать хосту. Узнаваемая инструкция bkpt 0xab апеллирует к хост машине и завершает вызов. Главное, не использовать это в релизной сборке, результат не обрадует.
"А чем вас стандартный printf не устраивает, дьяволы?!" - интересуюсь, пытаясь поддерживать заданный игривый тон обсуждения. Кажется, сейчас получилось грубовато. Отправляю вдогонку смайлик.
Опуская дальнейший обмен любезностями с коллегами, скажу сразу, что основные причины, толкнувшие их на отчаянный шаг, это: большой объем занимаемой функцией printf ROM памяти, необузданное тщеславие и медлительность стандартной функции вывода. Последнее объясняется тем, что хосту отсылается по одному символу за раз. Впрочем, для нас это уже не новость, такое мы видели.
Что же IAR говорит по этому поводу?
В некоторых системах, - молвит он мне нечеловеческим голосом, консольный вывод может быть не таким уж и быстрым, а все потому, что хост компьютер вынужден клещами вытаскивать из целевой системы каждый символ. Что избежать членовредительства, в библиотеку реального времени включен заменитель функции __write, известный как __write_buffered. Эта функция буферизирует вывод и посылает в отладчик одну строчку за раз, что несказанно ускоряет отладочную печать. Буфер не такой уж и большой, около 80 байт, даже вшивый твит старого образца, и тот не влезет.
Да, чтоб активировать буферизируемый вывод достаточно сказать компоновщику:
--redirect __write=__write_buffered.
Раз это так просто, то придется собрать и проверить стек вызовов при появлении отладочной печати.
main()
[printf + ...]
[_PrintfFullNoMb + ...]
[_Prout + ...]
[putchar + ...]
[__write_buffered + ...]
[__dwrite + ...]
[__iar_sh_stdout + ...]
Тут внимательный читатель воскликнет: "Обман! Такой стек я уже видел прошлый раз!". Да, но нет. Строчка действительно печатается за один присест, но __write_buffered вызывается для каждого символа. Просто он не посылает их сразу в хост машину, а складывает в буфер. И только когда последний символ строки был обработан, вызываются __dwrite и __iar_sh_stdout, которые без шума и пыли передадут все отладчику. A suivre.
👍8🤔1
7 друзей функции printf
Итак, мы всерьез вознамерились сделать лучшую отладочную печать для своих маленьких устройств. Напомню, что некие коллеги, посчитав, что у функции printf есть фатальный недостаток, написали свой велосипед. То есть собственную функцию печати через semihosting. Все, что угодно, лишь бы не использовать стандартные и скучные средства, предоставляемые компилятором.
- Ладно, вы молодцы, вы сделали свою функцию для печати через semihosting. Быструю, как Усэйн Болт, и миниатюрную, как Питер Динклэйдж, но я ведь смогу вывести в отладчике число? - а рукой в воздухе вырисовываю форматную строку "%d\r\n".
- А! Форматированная печать! - переглядываются коллеги, пряча в густых бородах улыбки, - элементарно! Надо только перед этим получить заветную строчку через snprintf.
Ах, какой же недогадливый. Только тут есть нюанс.
Как вы уже догадываетесь, в IAR printf использует специальные функции, которые могут понять строку форматирования и сгенерировать на ее основе ожидаемую печать. Такие функции начинаются с префикса _Printf. Да, вы поняли правильно, таких функций несколько. Они различаются набором поддерживаемых спецификаторов, соответственно, размер функций тоже разный.
В случае прямого и четкого вызова printf, где в качестве форматной строки выступает строковый литерал, компоновщик божится автоматически подобрать оптимальную функцию для форматирования. Однако если вызов не прямой, а происходит через указатель на функцию или форматная строка представлена переменной, то будет выбран самый "полный" формат с максимальным набором поддерживаемых спецификаторов.
Впрочем, свои пожелания компоновщику можно навязать, выбрав соответствующую опцию:
--redirect _Printf=_PrintfFullNoMb

что значит, вместо _PrintfFull будет использована функция _PrintfFullNoMb. У каждой функции семейства _Printf есть такая же функция с суффиксом NoMb. Значит, форматтер не поддерживает multibyte символы. Это которые могут состоять из нескольких байт, как в формате UTF-8, например.
Еще есть варианты форматтеров:
🔹_PrintfTiny/_PrintfTinyNoMb
Вообще, документация говорит, что Tiny не поддерживает режим multibyte, но функция _PrintfTinyNoMb тоже упоминается. Что-то мне подсказывает, что разницы между ними нет.
Эта функция форматирования поддерживает самый базовый набор спецификаторов: c, d, i, o, p, s, u, X, x.
🔹_PrintfSmall/_PrintfSmallNoMb
Немногим богаче базового варианта, но добавлена поддержка спецификаторов размера h, l, L, s, t, Z.
🔹_PrintfLarge/_PrintfLargeNoMb
Здесь уже есть поддержка спецификаторов чисел с плавающей точкой: e, E, f, F, g, G.
🔹_PrintfFull/_PrintfFullNoMb
Поддерживает вообще все спецификаторы, даже те, которыми я никогда не пользовался, например, для плавающей точки: a, A.
Использование _PrintfTiny вместо _PrintfFull высвобождает в моей сборке аж 6КБ! Это же сколько еще места дополнительного для всяких вычурных шаблонов.
Так вот, к чему было это предисловие. Было бы странно, если бы printf использовал одни функции форматирования, а snprintf - какие-то другие, собственные. Если заглянуть немного в последнюю функцию, то мы ожидаемо обнаружим какой-нибудь форматтер из семейства _Printf. Раз так, то и смысла усложнять себе жизнь, разбивая отладочную печать на два этапа, я не вижу. Когда компилятор предоставляет возможность использовать стандартный и хорошо знакомый printf, то глупо не воспользоваться такой замечательной возможностью. Ради такого можно даже заглянуть в документацию, чтоб делать это правильно.
👍6🤔2
IAR & GTest MATCHER_P
Тесты опять не смогли собраться. Все же обновлять репозиторий Google Test и не обновить компилятор было не лучшей идеей. Что же, если не собираются тесты, тогда время собраться мне. Собраться с мыслями и разобраться с тестами. Проблемы начались у IAR версии 8.50.x. Как оказалось, причиной недовольства старого ворчливого компилятора стало создание собственно макроса для сравнения... допустим, std::pair.
MATCHER_P(PairMatcher, value, "Pair Matcher") {
return (value.first == value.first) && (value.second == value.second);
}
Что-то именно в макросе пошло не так, судя по сообщению об ошибке. Заглянем внутрь магии макросов одним глазком
#define MATCHER_P(name, p0, description) \
GMOCK_INTERNAL_MATCHER(name, name##MatcherP, description, (#p0), (p0))
Ага, в сундуке заяц, в зайце утка. Погружаемся еще глубже
#define GMOCK_INTERNAL_MATCHER(name, full_name, description, arg_names, args)  \
template <GMOCK_INTERNAL_MATCHER_TEMPLATE_PARAMS(args)> \
class full_name : public ::testing::internal::MatcherBaseImpl< \
full_name<GMOCK_INTERNAL_MATCHER_TYPE_PARAMS(args)>> { \
public: \
using full_name::MatcherBaseImpl::MatcherBaseImpl; \
Вот оно! Главное тут сокрыто в строчке using full_name::MatcherBaseImpl::MatcherBaseImpl; - использование конструктора, доставшегося по наследству.
Вот, например, есть некий базовый класс Base с хитрым в меру конструктором
struct Base {
Base(int const x) : ...
...
};
наследник вместо обычного и муторного создания дополнительного конструктора
struct A : public Base {
A(int const x) : Base{x} {};
};
может использовать using
struct A : public Base {
using Base::Base;
};
В обоих случаях будет создан Base::Base(int), но вот в первом случае он будет вызван A::A(int), а во втором - A::Base(int). Делают они одно и то же, разницы нет, тогда зачем же писать больше?
Однако у gtest здесь не обычный класс, а шаблонный. А шаблоны, как хорошо известно, это праздник, который всегда с тобой.
Модифицируем слегка базовый класс:
template <class D>
struct Base {
Base(D const d) : ...
Теперь попробуем унаследовать конструктор через using, точно так же, как это записано в GMOCK_INTERNAL_MATCHER
template <class T>
struct A : public Base<T> {
using A::Base::Base;
};
У GCC с этим проблем нет, но IAR такая запись using A::Base::Base; активно не нравится:
Error[Pe2474]: inheriting constructors must be inherited from a direct base class
Наш капризный компилятор делает вид, что не понимает, хочет он иной записи:
using Base<H>::Base;
Если менять код самого google test нет возможности, например, он подтягивается прямо из официального репозитория, то есть небольшие проблемы. Без паники, выход есть! A suivre.
👍5🤔1
GTest PolymorphicMatcher
IAR нас подвел, но это хороший повод отказаться от использования богомерзкого и развращающего макроса. Настало время реализовать matcher вручную.
Для этого в стародавние времена, когда еще деревья были большими, а Стандарт маленьким, приходилось невероятно нравственно страдать: любой matcher должен был наследоваться от интерфейса MatcherInterface<T>, вызывать MakeMatcher для конструирования объекта.
Хорошо, что в наше просвещенное время все упростилось. В google test оценили все преимущества идиомы Fake VTable и активно применяют ее у себя. Поэтому в качестве основы для matcher может выступать любой класс при условии, что у него реализованы три основных метода. Это три функции из базового интерфейса MatcherInterface<T>.
bool MatchAndExplain(T n, std::ostream*) const
Самая важная функция принимает аргумент типа T и объясняет, почему он не подходит. Или подходит.
void DescribeTo(std::ostream* os) const
Когда значение соответствует условиям, метод оставляет описание в удобном для человека формате.
void DescribeNegationTo(std::ostream* os) const
Когда значение не соответствует, то метод объясняет и это в том же формате.
Теперь можно написать matcher класс, который сравнит два объекта std::pair.
class PairMatcher {
public:
PairMatcher(std::pair<int,int> const p) : expected_{p} {}
bool MatchAndExplain(std::pair<int,int> const p,
::testing::MatchResultListener* listener) const {
*listener << "[pair expected: {" << expected_.first << ", " << expected_.second
<< "}; actual: {" << p.first <<", " <<p.second << "}]";
return (p.first == expected_.first) && (p.second == expected_.second);
}
void DescribeTo(std::ostream* os) const { *os << "is OK"; }
void DescribeNegationTo(std::ostream* os) const { *os << "is not OK"; }
std::pair<int,int> expected_;
};
В конструктор передается ожидаемое значение пары, в метод MatchAndExplain - настоящее. Конечно, в чистом виде этот класс нельзя использовать, надо все равно создать специальный объект Matcher через функцию ::testing::MakePolymorphicMatcher.
::testing::PolymorphicMatcher<PairMatcher> Pair(std::pair<int,int> const n) {
return ::testing::MakePolymorphicMatcher(PairMatcher(n));
}
Использовать свой matcher не сложнее, чем стандартный гугловский:
EXPECT_CALL(mock, Calc(Pair({10, 10})));
При расхождении ожидания и реальности мы получим падение теста с хорошим объяснением происходящего:
example.cpp:77: EXPECT_CALL(mock, Calc(Pair(std::pair<int, int>{10, 10})))...
Expected arg #0: is not NULL
Actual: (12, 10), [pair expected: {10, 10}; actual: {12, 10}]
Expected: to be called once
Actual: never called - unsatisfied and active
Может быть полезно привести полиморфный matcher напрямую к ::testing::Matcher<std::pair<int,int>>, если ожидаемый метод Calc перегружен.
::testing::Matcher<std::pair<int,int>> Pair(std::pair<int,int> const n) {
return MakePolymorphicMatcher(PairMatcher(n));
}

Удобно, не нужно возится с наследованием, легко добавить свои самолепные классы и подробные описания того, что же пошло не так. Но есть нюанс! Фактически мы получили реализацию Monomorphic Matcher, который работает только с типом std::pair<int,int>. Чуть позже сделаем из него настоящий Polymorphic. A suivre.
👍3
GTest True PolymorphicMatcher
Как считают создатели GTest, чтобы сделать полиморфный сопоставитель (Matcher), достаточно добавить шаблон в нужное место. Зачем это нужно?
Чаще всего для обобщения какого-то класса сопоставителей, дабы не писать отдельный для незначительно отличающихся типов.
Например, если есть метод, принимающий как аргумент объект типа std::pair<int8_t,int8_t>
using PairArg = std::pair<int8_t,int8_t>;
MOCK_METHOD(void, Calc, (PairArg const), (const));
Следующее выражение для ожидаемого вызова не сработает.
EXPECT_CALL(mock, Calc(Pair(10, 10)));
Напомним, что Pair - это свободная функция, которая производит конкретный полиморфный сопоставитель.
::testing::PolymorphicMatcher<PairMatcher> Pair(int const a, int const b) {
return ::testing::MakePolymorphicMatcher(PairMatcher(std::pair<int, int>{a, b}));
}
Pair(10, 10) создаст сопоставитель для std::pair<int,int>, и тест справедливо считает, что он не подходит для аргумента std::pair<int8_t,int8_t>.
Хотя глупый тест мог бы догадаться, что 10 можно легко представить как тип int8_t.
Где же самое нужно место для шаблона? Конечно же это функция MatchAndExplain. Можно сделать так, что она примет стандартную пару любого типа.
template <typename T>
bool MatchAndExplain(std::pair<T,T> const p, MatchResultListener* listener) const;
Дальше сравнить пару не составляет труда.
В общем, неплохой полиморф получается, но если нужно проверить вызов перегруженного метода, то могут возникнут проблемы.
MOCK_METHOD(void, Calc, (int), (const));
MOCK_METHOD(void, Calc, (PairArg const), (const));
И все, тест делает вид, что не знает куда смотреть
error: call of overloaded 'gmock_Calc(testing::PolymorphicMatcher<PairMatcher>)' is ambiguous
Товарищи из GTest нашли приемлемую схему построения хороших сопоставителей, но не спешат делиться ей в своих поваренных книгах. Вместо свободной функции Pair, которую мы использовали раньше, можно использовать целый класс! Он будет подстраиваться под требуемый тип аргумента благодаря оператору приведения типа. Вот примерная схема:
class Pair final {
public:
Pair(int a, int b) : expected_ {a, b} {}
template <class T>
operator ::testing::Matcher<std::pair<T, T>>() const {
return ::testing::Matcher<std::pair<T, T>> {MakePolymorphicMatcher(Impl{value1_, value2_})};
}
private:
class Impl {
public:
Impl(int a, int b) : expected_{a, b} {}
template <typename Super>
bool MatchAndExplain(Super x, ::testing::MatchResultListener* listener) const {
...
}
void DescribeTo(::std::ostream* os) const {...}
void DescribeNegationTo(::std::ostream* os) const {...}
private:
...
};
...
};
Все самое важное сокрыто во внутреннем классе Pair::Impl: полиморфное сравнение и разные описания. Внешний класс лишь помогает сравнителю адаптироваться в сложных условиях теста.
👍5
GTest New Polymorphic Matcher
Я понимаю, никто особо не горит желанием писать тесты. Скучная тема. Но будь ваш проект хоть трижды embedded, все равно это неизбежно. Современная философия разработки настигнет и вас, заставит писать проклятые модульные тесты на каждую строчку до 100% покрытия, до боли, до слез, до отвращения. Только насытившись страданиями несчастного разработчика, департамент цензуры даст добро. И это не так уж и плохо. У пользователя появляется шанс не умереть страшной, но нелепой смертью, пользуясь вашим устройством.
Так вот, чтоб инженер не сильно грустил на работе, GTest облегчили процесс создания полиморфных сопоставителей.
class PairMatcher {
public:
using is_gtest_matcher = void;

PairMatcher(std::pair<int,int> p) ...
template <class T>
bool MatchAndExplain(std::pair<T,T> const n, std::ostream*) const {...}
void DescribeTo(std::ostream* os) const {...}
void DescribeNegationTo(std::ostream* os) const {...}
...
};

Казалось бы, зачем усложнять, добавлять тег is_gtest_matcher? Все просто, если тест видит определения типа is_gtest_matcher внутри класса, то такой класс становится "своим". Для них не нужны никакие костыли в виде MakePolymorphicMatcher. Свои классы, сочтемся! is_gtest_matcher гарантирует, что есть необходимый набор методов юного сопоставителя. Зуб дает, интерфейс подходит.
template <class T>
PairMatcher Pair(T const a, T const b) {
return PairMatcher{std::pair<int, int>{a, b}};
}

Или как-то так, достаточно только вернуть объект класса PairMatcher, а уж тест сам определит как его употребить.
Впрочем, как мы уже определили, свободная функция теряется при любой перегрузке метода в mock объекте.
Возьмем лучше класс Pair, которые пылится с прошлого раза на антресолях. Преобразуем внутренний класс Impl, добавив стильный тэг.
class Impl {
public:
using is_gtest_matcher = void;

Отлично, теперь изменим только оператор приведения типа, уберем все лишнее.
template <class T>
operator ::testing::Matcher<std::pair<T, T>>() const {
return ::testing::Matcher<std::pair<T, T>> {Impl{value1_, value2_}};
}

Теперь объект внутреннего класса может быть напрямую приведен к типу testing::Matcher, что и требуется для EXPECT_CALL. К тому же выдерживает перегрузки методов. Да еще и модный. Лапидарный.
👍6🤔1
IAR исправленный и доработанный
Сегодня в конторе радость. Разработчики кричат ура и бросают в воздух шапочки из фольги. Нет, в офис завезли не мороженое и не пиццу, а новую версию IAR! Хватит искать глупые ошибки в древнем 8.10.x, будем искать умные в 9.30.y.
В примечаниях к выпуску гордо красуется надпись: "Library support for the C++17 language standard".
Это башковитые ребята из IAR хвастаются нам, что наконец-то допилили все фичи семнадцатого стандарта. Не прошло и дюжины лет.
"А вы точно весь стандарт можете?" - спросим мы с ленинским прищуром.
Проверим на структурных привязках (Structured binding declaration). Это была абсолютно невозможная вещь в предыдущих версиях.
Ладно, в IAR 9.10 версии была попытка реализовать что-то подобное, но дело ограничилось tuple-подобными типами.
std::tuple<int, float> x {1, 3.F};
auto [a, b] = x;
Смотрится неплохо, но и без сахара прожить было можно.
int a; float b;
std::tie(a, b) = x;
Дает тот же результат, правда, объявить переменные правильных типов нужно заранее.
Гораздо интереснее случаи привязки массивов или структур.
Настоящие олдскульные массивы можно привязывать к переменным поэлементно:
int x[] {1, 3};
auto [a, b] = x;
То же самое и со структурами, можно привязаться к публичным членам структуры.
struct Point {int x, y;};
Point x {1, 3};
auto [a, b] = x;
Как ни странно, но все три случая не вызывают сложностей у новой версии компилятора. AUTOSAR, правда, таких конструкций не понимает, как и if constexpr. Не отказываться же из-за таких мелочей от потрясающих возможностей языка. Огорчает только отсутствие std::ignore, которое можно использовать в std::tie, но для структурных привязок специального плейсхолдера не завезли.
👍7
Spiral Rule. Начало.
Улица, окончательно измокнув в назойливом летнем солнце, лениво раскатывалась передо мной. Я уже сам был не рад тому, что улизнул с работы подышать воздухом и перекусить миндальным круассаном. И то, и другое оказалось прожаренным и сухим. Вдруг невдалеке раздался истошный вой. "Стой! Стой!" - кричали явно мне, поэтому я прибавил шагу. Облаченный в дырявый грязный свитер, ко мне бежал какой-то городской сумасшедший со взъерошенными волосами и растрепанной, будто искромсанной осколком стекла, бородой. "Тебя-то я и искал, тебя-то мне и надо! Пойдем со мной", - странный человек грубо схватил меня за руку и потащил в ближайший бизнес-центр. Сил сопротивляться после недавнего митинга у меня не осталось, было только опасение, что новый знакомый может откусить мне ухо, если я не пойду с ним. Он волоком втащил меня на верхний этаж, пинком раскрыл дверь и практически вбросил меня во внутрь.
Я лежал на полу офиса и притворялся мертвым, когда услышал неприятный скрипучий голос: "Добро пожаловать на собеседование в компанию Йадро!"
Приоткрыв один глаз, я взглянул в сторону источника звука. Надо мной нависал огромный цветастый петух. От неожиданности я встал и тут же сел на стул. Скорее всего, это была ростовая кукла, но сделанная столь искусно, что меня терзали сомнения.
"Что вы здесь видите?" - спросило пернатое и сунуло мне в руки бумагу.
В середине листа было напечатано только:
int *(*f[2])(char const * const)
"Это функция", - я тупо смотрел на сточку - "эээ... указатель на функцию".
Петух прищурился. "Массив из указателей на функцию! Я так вижу", - выпалил я.
"Ответ неудовлетворительный!" - гаркнула птица, одним крылом смахнув листок, а другим залепив мне увесистую оплеуху.
Тут же я увидел перед собой второе задание:
const int * p1;
int const * p2;
int * const p3;
int const * const p4;
int * ptr = ? (p1/p2/p3/p4)
Понятно, требовалось назвать, какое из этих указателей можно присвоить указателю ptr.
Очевидно, что это может быть только константный указатель на тип int, но где при этом должен быть const? Я и в обычных условиях мог запутаться в показаниях, а тут звезд стало еще больше.
Я вынул из кармана счастливую игральную кость, которую всегда носил с собой, и бросил ее на стол.
"Третий!"- сказал я, глянув на остановившуюся кость.
Прищур петуха петуха из подозрительного истончился в осуждающий. "Вы нам не подходите! Мало того, что вы самозванец, так еще и тупой!!" Противный голос мерзкого чудовища бил в самое сердце моего самолюбия. "Любой решит эти задания в два счета!"
"Ах, любой?! Я вам покажу! Я докажу", - волна несправедливой обиды наконец захлестнула меня. Я вскочил, пинком открыл дверь и резво бросил тело в улицу. Выбрав из редкой толпы самого глупого на вид прохожего, погнался за ним, крича изо всех сил: "Стой! Стой! Тебя-то я и искал...".
👏9🔥4👍1
Spiral Rule. Конец.
Разберем промелькнувшие в предыдущем посте задачки для собеседования с хладнокровием Лаврентия Берии.
Чтоб не попасть впросак на каком-нибудь собесе, Дэвид Андерсон еще в 1994 году предложил мнемоническую технику, помогающую в уме раскрутить самые сложные типы. Раскрутить в прямом смысле слова, двигаясь по левой спирали. Это которая закручивается по часовой стрелке.
Мистер Андерсон описал несколько простых правил:
🔹 Раскручивайте выражение по спирали от определяемого элемента по часовой стрелке, по пути переводя магические руны на человеческий язык:
вместо [X] говорим "массив из X элементов";
вместо [] — массив неопределенного размера;
(T1, T2) — функция, принимающая T1 и T2 в качестве аргументов, возвращающая ...
* — указатель на ...
ну и далее в том же духе.
🔹 Сначала нужно определить внутренности скобок.
🔹 Крутить спираль нужно, пока не кончатся элементы выражения.

Самое просто выражение int *ptr заиграло новыми красками. Начинаем от ptr, двигаемся по воображаемой спирали: ptr - это указатель на ... далее спираль делает виток и попадает точнехонько в int. Итого: ptr - это указатель на int.
Замечательно, теперь коварный const не собьет вас с толку, сразу понятно, к чему он относится.
int const * const ptr;
ptr -> * const -> int const
ptr - это константный указатель на константный целочисленный тип int
.
Можете проверить, переписав тип указателя следующим образом
template <typename T>
using pointer = T *;

Тогда запись pointer<int const> будет эквивалентна int const *, а pointer<int> const эквивалентна int * const.
Вычурный тип из первой задачки:
int *(*f[2])(char const * const)

можно раскрутить так:
f -> [2] -> * -> (* const -> char const) -> int *
f - это массив из двух элементов: указателей на функцию, принимающую в качестве параметра константный указатель на константный тип char, возвращающую указатель на тип int
.

Тут вы, конечно, скажете, что задачки высосаны из пальца. Я, конечно же, соглашусь.
Насчет птицы только не соглашусь. Петух — древнейшая эмблема вестника рассвета, пробуждения, бдительности, призыва к возрождению и к бою.
👍8🤔1
AUTOSAR
Правило A12-8-2
Определенные пользователем операторы копирования и перемещения должны использовать определенную пользователем функцию swap, которая не кидает исключений.
"Чувак, посмотри, что творит твой AUTOSAR!" - коллега с заплаканными глазами бросил в меня куском кода.
"Никто уже не говорит чувак, старики только, - проворчал я. - Чего шумишь?"
"Не желает принимать мой оператор перемещения, хотя он правильный!"
Я развернул код, там был какой-то средней сложности класс, допустим:
struct X {
int32_t number;
X *next;
};
От реализации многострадального оператора перемещения веяло чем-то старинным и благородным, вроде давно забытого обращения "чувак".
X& operator=(X &&obj) & {
if (this != &obj) {
number = obj.number;
next = obj.next;
obj.next = nullptr;
obj.number = 0;
}
return *this;
}

Многословно, легко запутаться. AUTOSAR не настаивает, но советует использовать другой подход. Copy-and-swap идиома называется.
Определение своей функции swap может быть хорошей идеей, позволяет достичь исключительной безопасности кода или, лучше сказать, исключить опасность непредвиденных ситуаций, то есть полной свободы от исключений. Ну, вы поняли.
static void Swap(X& lhs, X& rhs) noexcept {
std::swap(lhs.number, rhs.number);
std::swap(lhs.next, rhs.next);
}

Смотрите, как похорошел оператор перемещения при Swap-е:
X& operator=(X &&obj) & {
Swap(*this, obj);
return *this;
}

Неочевидный плюс в том, что можно не проверять случай присваивания объекту самого себя.
Оператор копирования будет не намного сложнее, нам понадобится временный объект на замену:
X& operator=(const X &obj) & {
X tmp{std::move(obj)};
Swap(*this, tmp);
return *this;
}

AUTOSAR не требует странного. Для этого правила выбрали известную идиому, которую посчитали более безопасной.
👍4
IAR ломает AAPCS
Новоиспеченный Тимлид зашел в опенспейс, мрачным титаном возвышаясь над стройными рядами кубиклов. Угрюмый взгляд его скользил по сгорбленным спинам разработчиков, выбирая жертву для сакрального ритуала "один-на-один". Ну как он его понимал.
"Ты! - нужная спина наконец-то была найдена, - в переговорку!"
Предводитель сгреб в охапку несчастного, тот и ойкнуть не успел. Кажется, успел только пару раз хрустнуть скелетом в тяжелых нежных лапах. Из переговорной комнаты доносились глухие удары. Злые языки утверждали, что Тимлид получил работу через собеседование поединком.
Тимлид же размеренно бил снятой сандалией по столу: "Почему билд красный? Да ему попросту стыдно, что ты его вчера трогал..."
"Какие еще тесты? Что ты наделал?" - дрожали стенки переговорной. Судя по всему, он стремился вызвать у сотрудника неадекватные реакции на конструктивную критику, как тот мальчик с собакой из старого ералаша.
"Может сказать ему, что все сломалось не из-за того бедолаги", - двое разработчиков переглянулись, сохраняя невозмутимые лица, - "а из-за обновления компилятора?"
"Ты что, он же думает, что компилятор - это фамилия".
Про IAR и заикаться не стоило, на тебя только посмотрят презрительно и скажут, что правильно AIR, а не IAR. Тем не менее, дело действительно было в нем, родимом.
Для новой версии компилятора внезапной проблемой стал такой тест
int result {add(1.F, 2.F)};
EXPECT_EQ(result, 3.F);
Где функция add определена в другой единице трансляции
int32_t add(float const x, float const y) {
return static_cast<int32_t>(x + y);
}
С новым IAR 9.30.x тест провалился. В result образовалась не тройка, а целый ноль. Для определения проблемы нужно нырнуть поглубже и посмотреть как функция вызывается.
vmov.f32        s1, s14
vmov.f32 s0, s15
bl add(float, float)
Обратите внимание, что по умолчанию IAR использует FPU регистры для передачи в функцию значений типа float, если в архитектуре этот FPU предусмотрен и используется. Регистров fpv S# может быть разное количество, здесь у нас их аж 16 штук. Итак, вызов функции готовится специальными FPU-инструкциями в специальных FPU-регистрах. А что же функция?
add(float, float):
push {r7, lr}
sub sp, sp, #8
add r7, sp, #0
str r0, [r7, #4] @ float
str r1, [r7] @ float
ldr r1, [r7] @ float
ldr r0, [r7, #4] @ float
....
А вот функция использует иную конвенцию вызовов и пытается вытрясти передаваемые параметры из обычных регистров R#.
Несовместимость на уровне ABI - это редкое завораживающее явление. IAR покоряет новые вершины.
Но не стоит переживать, все можно исправить. Достаточно взять обычную... A suivre.
👍8😁3🤔1
Я чту AAPCS. Это моя слабость.
"Почему вы решили сменить работу?" - интервьюер с серьезным лицом вкинул первый вопрос, правда, не первой свежести.
"Ну, если коротко, то несовместимость у нас с менеджером была", - ответ был уже давно тщательно заготовлен, как домашние соления, и так же ждал момента с громким хлопком вырваться наружу. - "Эта, как ее там..."
"Бинарная?" - собеседник явно думал о чем-то своем.
"Небинарная... тьфу, в смысле, психологическая, во! Он за мной по офису с шокером бегал..." - прервав заготовленную тираду о нелегкой доле плюсовика, спросил: "А вы почему про бинарную вспомнили? У вас тоже проблемы с новым IAR?"
По появившемуся на лице моего визави нежно-зеленому оттенку я понял, что попал в яблочко. Как много еще людей не знают, что можно все исправить, явно указав компилятору, какое соглашение о вызовах функций использовать.
Для IAR достаточно одного взгляда на документацию, чтоб отыскать нужную опцию, бесхитростно названную --aapcs.
Она как раз и отвечает за то, какое соглашение использовать для вызова функций, где аргументы или возвращаемое значение представляются числами с плавающей точкой.
У нее есть два варианта использования:
--aapcs=std 
Для передачи аргументов и возвращаемого значения будут использованы регистры общего назначения в строгом соответствии со стандартом AAPCS. Функция сложения двух значений типа float будет выглядеть вот так:
add(float, float):
VMOV S0,R0
VMOV S1,R1
VADD.F32 S0,S0,S1
VMOV R0,S0
BX LR

Это будет вариантом по умолчанию, если не выбран FPU для архитектуры.
--aapcs=vfp 
Для передачи параметров и всего остального будут использованы специфические регистры FPU. Тут уже не приходится говорить о какой-то там совместимости с AEABI. Если заглянуть в стандарт AAPCS, то про FPU регистры там ничего не говорится. Используйте такое расширение на свой страх и риск.
Функция сложения схлопнется до:
add(float, float):
VADD.F32 S0,S0,S1
BX LR

Если для архитектуры выбран VFP, то этот вариант включается как основной.
Есть ли у GCC такие же потрясающие возможности?
Читая описание опций, можно подумать, что за подобное отвечает -mgeneral-regs-only, которая запрещает использовать регистры FPU, но торопиться на надо. Это очень своеобразный запрет:
error: argument of type 'float' not permitted with '-mgeneral-regs-only'

просто не используй float, и все будет отлично.
Это было грубо, нам бы более изящное решение. Мы найдем его во всем известной опции -mfloat-abi, определяющей, какой ABI для чисел с плавающей точкой использовать.
Есть опции soft и hard, которые либо полностью отрицают FPU, либо делают код полностью FPU-специфичным.
Но есть еще незаслуженно забытая опция softfp, которая позволяет генерировать компактный код с FPU инструкциями, но уважая AAPCS, то есть передавая аргументы через регистры общего назначения.
Через неделю мне все же перезвонили, радостно объявили, что они не готовы никого нанимать. Тогда они были в отчаянии с зелеными лицами и красными билдами, а теперь наоборот, теперь все хорошо.
👍7
Отпуск без IAR
Осень потихоньку желтит березки во дворе, но погода все равно шепчет. Хотя в отпуске сгодится любое сочетание температуры воздуха, облачности, осадков и ветра, не приводящее к летальному исходу. Можно беззаботно шлепать по лужам или валяться в канаве, предоставляя солнцу и дальше коптить натруженные пальцы. Главное, забыть про работу, про бесконечные ревью бестолкового кода, про громкие споры с коллегами, после которых теряешь веру в человечество. Все это безжалостно выбросить из головы, как вонючий лук из сэндвича. Жизнь коротка, отпуск еще короче, прожить его надо так, чтоб приехавшие потом на огонек пожарные годами вспоминали ЭТО с содроганием.
Хорошо, хорошо, расслабиться можно и поскромнее. Лежать в шезлонге на шикарном волжском пляже, потягивая через трубочку картофельную пинаколаду. Запустить руку в теплый песок и случайно вытащить забытый ноут и подключенную к нему старую плату stm32f3-discovery. К знакам судьбы надо прислушиваться, решаете вы, и твердо намереваетесь мигнуть светодиодом. Только вот всякие удобные проприетарные компиляторы-отладчики остались в офисе вместе с мыслями о работе. Не проблема!
Нам понадобятся openOCD, GDB и кросс-компилятор GCC, конечно.
Допустим, мы уже изготовили прошивку с помощью STM32CubeMX, блокнотика и крепкого словца. Теперь нам интересен, например, вывод отладочной информации через printf.
Что нам нужно, чтоб увидеть ее со стороны отладчика? Конечно, включить semihosting!
Нужная функция уже есть у GCC, нам только нужно экспортировать ее
extern void initialise_monitor_handles(void);

и вызвать где угодно до первого printf.
int main(void) {
HAL_Init();
SystemClock_Config();
initialise_monitor_handles();
...

Чтоб все собралось без проблем, не забудем в Makefile, если он есть, заменить зависимости:
LIBS = -lc -lm -lnosys на LIBS = -lc -lm -lrdimon
и
LDFLAGS = $(MCU) -specs=nano.specs ... на LDFLAGS = $(MCU) -specs=rdimon.specs ...

Время запустить сервер отладки с правильными настройками для нашей платы
openocd.exe -f ../openOCD/share/openocd/scripts/interface/stlink.cfg -f ../openOCD/share/openocd/scripts/board/stm32f3discovery.cfg

Чтоб упростить себе жизнь, можно написать небольшой скрипт для GDB
target remote :3333
mon arm semihosting enable
mon reset halt
load

Если коротко, то тут описано, что отладчик подключается к отладочному серверу, активирует semihosting и загружает прошивку.
Теперь запустим отладчик
arm-none-eabi-gdb.exe firmware.elf -x debug.cmd

и наслаждаемся тем, как резко сервер отладки выплевывает строчки. Если вам пригодился этот рецепт, то стоит пересмотреть свое отношение к жизни и перестать работать хотя бы в отпуске.
👍10🔥1
Integral promotions. Начало.
Как-то погожим сентябрьским утром сидел я на крохотном волжском островке и рыбачил. Вдруг откуда ни возьмись мимо проплывает лодчонка утлая, но с гордым именем "Йадро".
Важно подбоченившись, сидит в ней старый дед лет тридцати и вычерпывает воду консервной банкой.
"Это дед Зазнай, - перешептывались сидящие неподалеку джуны. - Явился на своем корыте."
"Опять топить нас будет!" - подтвердил мои худшие опасения нервный вскрик другого молодого специалиста.
"Ну что, убогие! Хотите ко мне на лодку? - дед похлопал по своему видавшему виды транспортному средству, из-за чего от него отвалилась какая-то мелкая деталь. - Вот ты, страшненький, с тобой говорю!"
Удивительно, но Зазнай смотрел прямо на меня. Я чихнул. Обрадованный дед быстро поплыл ко мне, приняв неосторожное движение головой за согласие. Подплыл достаточно близко, чтоб увидеть, как он затыкает дыру в днище босой ногой.
Не успел я и глазом моргнуть, его большое весло уже порхало над мокрым песком, выводя условия задачи.
Задача деда Зазная:
#define XSTR(STR) #STR
#define PRINT(EXPR) printf("%s is %s\r\n", XSTR(EXPR), (EXPR) ? "TRUE" : "FALSE")

int a = -1;
unsigned int b = 1U;
char c = -1;
unsigned char d = 1U;

PRINT(a < b);
PRINT(c < d);
Дед смотрит на меня с такой ехидной и мерзкой улыбочкой.
Я смотрю за задачу. Почему на собеседованиях хорошим тоном считается подсунуть разработчику дурно пахнущий код? Тут шаманство с макросами, там разные типы сравниваются. Нехорошо это. Операнды разных типов могут принести многие печали невнимательному автору.
Я не специалист по некорректному поведению, но могу исправить код, чтоб выполнялся корректно.
"Вы вообще стандарт читали, молодой человек?" - дед пренебрежительно смотрит на меня.
Конечно, читал. Вернее, обчитывал избранные места.
"В стандарте все написано!"
Ладно, давайте рассуждать логически. В первом случае скорее всего будет приведение к беззнаковому типу, иначе это была бы слишком простая задача. Во втором случае наверняка есть какая-то подлость. Хоть есть некое сходство с первым случаем, где сравнивается знаковое и беззнаковое числа, но результат будет обратный. Иначе такой самодовольный хитрый плут, как ты, и не спрашивал бы.
a < b is FALSE
c < d is TRUE
"Ответ, может, и верный... - лицо Зазная за секунду выразило все оттенки пренебрежения и перешло к презрению. - Но ты не знаешь наизусть стандарт!"
Пристыженный, я остался стоять на берегу. Дед Зазнай на вожделенной лодке плыл дальше в закат, оставляя после себя след из утоплых трупов мертвых джунов.
👍7🤔2
Облака
Я решил поймать пышное природы увядание в лесу, полном условно съедобных грибов и опасных животных. Белочек, например. Заряда аккумулятора надолго не хватит, поэтому напишу самое важное. Наблюдать осень изнутри природы немного грустно и холодно. На голодный желудок очи не слишком очаровываются. Электричества тут нет, еды тоже нет.
"Зато облака есть! Как может быть грустно-скучно, когда что-то есть? - возразит мне говорящая обезьянка из советского мультфильма. - Есть облака и есть сервисы!"
Она абсолютно права, если вы сталкивались с облачными технологиями, например, в IoT, то знаете, какое это веселье. Поэтому, если вдруг интересно, можно зарегистрироваться на наш местный вебинар о сложной судьбе облачных сервисов в России: https://auriga.timepad.ru/event/2570804/ Рассмотрим и буржуйские, и отечественные. Да, да, не удивляйтесь если и такие.
Ждать осталось недолго, уже в этот четверг 21-го состоится наш праздник облакообразования. Если не попаду на ужин к хищному ежу, то обязательно подключусь, разгоню осеннюю тоску.
👍7😁1
Integral promotions. Конец
Пришло время разобрать и нелепую задачу с собеседования. Вся ее несуразность состоит в сравнении разнотипных значений. В самом деле, какой безумец способен на такое?
Ладно, допустим, у нас есть некий бинарный оператор с операндами разных типов. Как предсказать поведение компилятора в этом случае? Просто выполнить в каком-нибудь godbolt-е. Ну, или пойти тернистым путем разглядывания стандарта. Обратим внимание на пункт 7.4 Обычные арифметические преобразования [expr.arith.conv].
Цель подобных преобразований (usual arithmetic conversions) состоит в приведении всех операндов к общему типу, который также является результирующим.
Если оба операнда имеют целочисленные типы, то к ним должно быть применено целочисленное расширение (integral promotions). Вот это уже интересно. Что это за зверь такой?
Перелистнем несколько страниц стандарта назад и обнаружим пункт 7.3.6 Целочисленное расширение [conv.prom].
Значение (prvalue) целочисленного типа отличного от bool, char16_t, char32_t или wchar_t, чей ранг меньше, чем у типа int, будет преобразовано в значение типа int, если он позволяет представить все значения исходного типа, иначе исходное значение будет преобразовано в тип unsigned int.
Так я и знал, компилятор ненавидит малые типы и при первой возможности избавляется от них. Читаем дальше.
К расширенным операндам должны быть применены следующие правила:
🔹 Если операнды одного типа, то дальнейших преобразований не требуется. Логично.
🔹 Если оба операнда знаковых типов или же оба беззнаковых типов, то операнд с типом меньшего ранга должен быть преобразован в тип большего ранга.
Например: (char)'a' + 1L
тип значения 'a' будет расширен до int, тип второго операнда останется long int. У типа long int ранг выше, соответственно, приводим все к типу long int.
🔹 Если ранг операнда беззнакового типа больше или равен рангу типа другого операнда, операнд знакового типа должен быть преобразован в беззнаковый тип.
Например: 2u + 10
Типы unsigned int и int имеют одинаковый ранг, соответственно, приводим все к unsigned int.
🔹 Если тип операнда знакового типа может представлять все значения типа операнда беззнакового типа, операнд беззнакового типа должен быть преобразован в знаковый тип.
Например: 0UL - 1LL
Сравним типы unsigned long и long long int для ARM, где sizeof(unsigned long int) == 4, а sizeof(long long int) == 8. Ранг long long int больше.
long long int может представить все значения unsigned long, поэтому результирующий тип будет long long int.
🔹 Иначе оба операнда должны быть преобразованы в беззнаковый тип, соответствующий типу операнда знакового типа.
А теперь рассмотрим этот же пример, но для x86-64, где sizeof(unsigned long int) == 8, а sizeof(long long int) == 8.
В этом случае long long int не сможет представить все значения unsigned long int. Результирующий типом внезапно становится unsigned long long int.
Как видите, легко получить совершенно разные результаты вычислений на разных архитектурах.
Зная эти правила, легко решить и оригинальную задачу:
int a = -1;
unsigned int b = 1U;
char c = -1;
unsigned char d = 1U;

PRINT(a < b);
PRINT(c < d);

В первом случае у нас знаковый и беззнаковый тип с одинаковыми рангами, переменная а будет приведена к типу unsigned int по правилу, изложенному в пункте 7.3.8 Целочисленные преобразования [conv.integral], то есть -1 % 2 ^ 32, что будет.... в общем, это будет много, гораздо больше, чем 1.
Во втором случае char и unsigned char подвергнутся расширению до int. Ничего страшного не случилось, значения не поменялись, -1 по-прежнему будет меньше, чем 1.
Безопасность и цензура неявные преобразования типов не жалуют, но правила знать не помешает, особенно если вы хотите попасть на работу к бесноватым в команду.
👍10😁1
sizeof & function
Прибежали в избу дети. Второпях зовут отца: «Тятя! тятя! Мы сломали твою никчемную библиотеку!"
Сломали и сломали, чего бубнить?! На каждого хитрого разработчика, наивно полагающего, что предусмотрел все, найдется каверзный пользователь с кривыми руками.
Ничего не поделаешь, начинаем копать библиотеку, находим запрятанную глубоко, как копи Мории, функцию, смысл которой состоит в сохранении передаваемых методов для последующего их вызова:
template <class T>
void Store(T &&t) {
static constexpr size_t kAvailableMemory {16};
static_assert(sizeof(T) <= kAvailableMemory);
...
}
Для начала попробуем сохранить простую и свободную функцию Bar.
static void Bar() {}
Если использовать мудрость предков и явно передавать указатель на функцию через операцию взятия адреса (т.е. через амперсанд), то никаких проблем не будет.
Store(&Bar);
Дорогие коллеги презрели опыт поколений и решили, что и так сойдет
Store(Bar);
IAR тут же высказал все, что думал об этой затее:
Error[Pe056]: 
operand of sizeof may not be a function
detected during instantiation of "void Store(T &&) [with T=void(&)()]"
Оператору sizeof категорически не нравится нечто, которое мы пытаемся пропихнуть в функцию.
Оказывается, если не поставить закорючку амперсанда, то вместо типа void(*)() мы получим на входе тип void(&)().
То есть ссылку на функцию, не указатель. Казалось бы, мелочь, но что говорит об этом стандарт?
8.5.2.3 Sizeof [expr.sizeof]
Если оператор sizeof применяется к ссылке или ссылочному типу, то результатом будет размер оригинального типа.
Оператор sizeof может быть применен к указателю на функцию, но не должен быть применен к функции напрямую.
Раз так, то sizeof грустно смотрит на ссылочный тип, определяет оригинальный тип как функцию и справедливо отказывается работать дальше: по стандарту не положено!
Если бы функция Store явно ожидала в качестве аргумента указатель на функцию, то это бы сошло пользователям с рук.
void StorePtr(void (*t)()) { ... }

Компилятор бы тихо и незаметно привел ссылочный тип к указателю на функцию. Шаблоны же любят точность.
Кстати, GCC в этом случае ведет себя странно.
In instantiation of 'void Store(T&&) [with T = void (&)()]':
: required from here
: warning: invalid application of 'sizeof' to a function type [-Wpointer-arith]
Вроде бы несоответствие стандарту зафиксировано. Однако это не ошибка, а всего лишь предупреждение, если не включен флаг -pedantic-errors.
sizeof здесь вернет 1 и программа соберется и выполнится, несмотря на возможные чудовищные последствия.
👍5
alignas
Третьего дня взял у коллеги модуль и решил собрать его в GCC. Ну хобби у меня такое. Хлебом не корми, дай посмотреть, как модули корчатся под разными компиляторами.
Так вот, не собрался модуль со странной ошибкой.
"error: expected ')' before ',' token", - говорит GCC, а сам на структуру показывает:
template <class T>
struct Chunk {
alignas(T, void*) T val;
};
Я глаза вытаращил, в ужасе ищу, где в стандарте такое написано. Не нашел. По-прежнему использовать alignas положено только в трех формах:
alignas(constant-expression) - тут все просто. Передается некая константа, определяющая выравнивание alignas(4) или alignas(32).
alignas(type-id) - использовать имя типа, ибо size_t для разных архитектур может отличаться размером и выравниванием, конечно. В общем, выражение эквивалентно alignas(alignof(type-id)).
alignas(pack ...) - вот тут интереснее, в alignas можно передать пакет параметров! Только вот (T, void*) не является пакетом в любом случае. Так писать некорректно.
Очевидцы пишут, что в ранних версиях GCC можно было написать так, и все компилировалось, но мне не удалось поймать GCC за руку. Вероятно, все быстренько привели к стандарту. Только IAR не спешит исправлять ситуацию.
Классическое использование пакетной передачи параметров выглядело бы так:
template <class ... T>
struct Chunk {
alignas(T...) int8_t v[16];
};
Тогда объявление alignas(T...) в типе Chunk<int16_t, int32_t, int64_t> развернется в alignas(int16_t) alignas(int32_t) alignas(int64_t) int8_t v[16];
Впрочем, можно явно написать несколько спецификаторов alignas, нужно только, чтоб был хотя бы один спецификатор выравнивания не меньше, чем выравнивание оригинального типа. Если исходный тип int8_t, то этого добиться очень просто. В нашем же случае стандарт рекомендует пользоваться такой конструкцией:
  alignas(T) alignas(void*) T val;
Спецификатор alignas(T) гарантирует, что запрашиваемое выравнивание будет не меньше, чем для оригинального типа.
Тут критик воскликнет: "Тут все неправильно! Нужно просто оставить alignas(void*), которое будет игнорироваться если выравнивание T будет больше."
  alignas(void*) T val;
Хоть GCC и позволяет такие вольности, но запись некорректна. Стандарт не одобряет. А IAR прямо ругается, если подставить тип размером больше void*:
Error[Pe1881]: alignment cannot be set to less than the default alignment
Вот и хорошо, остановимся на варианте с двумя alignas. Потом выясним у коллеги, зачем ему вообще понадобилась эта структура.
👍42🤔1
Alignment
Представьте, приходите вы в гости в какую-нибудь компанию "ООО Специальный Тракторный Центр". Мило беседуете о превратностях плюсов до тех пор, пока их эксперт не спрашивает про выравнивание в структуре.
А какое оно может быть? Ясно и ежу, что в структуре побеждает член с самым сильным требованием к выравниванию. Вся структура, естественно, будет иметь такое же выравнивание.
Однако эксперт поправляет вас, заявляя, что выравнивание структур идет по 4 байта. По ширине регистра в 32 битной системе.
Тут вы, конечно, поперхнетесь молоком, которое там наливают за вредность. Очень вас понимаю.
В структуре struct T {int8_t x; int8_t i;}; выравнивание будет равно единице.
struct T {int8_t x; int64_t i;}: здесь самое сильное требование выравнивания у типа int64_t, такое же будет и у структуры (alignof(T) == 8).
Может быть, парнишка имел в виду раздел стандарта 6.6.5 Alignment [basic.align], где вводится понятие фундаментального выравнивания?
Фундаментальное выравнивание представляет собой выравнивание, меньшее или равное наибольшему выравниванию, поддерживаемому реализацией во всех контекстах, равное alignof(std::max_align_t).
std::max_align_t обычно является типом-синонимом самого большого скалярного типа. На большинстве платформ это тип long double, и его требование к выравниванию это 8 (для 32 битных систем) или 16 (для 64).
Нет, по цифрам тут не сходится, да и по смыслу тоже. Там же есть понятие расширенного выравнивания, которое представляет собой выравнивание большее, чем alignof(std::max_align_t).
Например:
struct T {
int8_t x;
alignas(64) int64_t i;
};
static_assert(alignof(T) == 64);
Нет, опять же все это противоречит словам интервьюера. Я бы потом еще неделю не мог уснуть, все думал, почему тот мерзавец такое сказал. Уверенно сказал, значит, где-то видел. Но где?
Мне в голову приходит только один гипотетический случай. Допустим, есть базовый класс с виртуальным деструктором, все как положено:
class A {
public:
virtual ~A() {}
};
Наследуем от него класс B, где есть член класса типа int8_t.
class B : public A {
public:
int8_t flag {};
};
Выравнивание этого класса запросит аж 4 байта (для 32 битной системы). А все потому, что мы намеренно в базовом классе создали виртуальный деструктор, чтоб спровоцировать появление vtable.
Это значит, в составе класса есть скрытый указатель и static_assert(alignof(B) == alignof(void*)); и кажется, что выравнивание везде идет по 4 байта.
На собеседованиях драться - дурной тон, зато можно делать круглые глаза. Живите со своими заблуждениями. Еще лучше - попытаться выяснить, что имеет в виду собеседник.
Если он с заносчивым лицом знатока менторским тоном будет учить вас жизни, то стоит подумать, нужно ли работать вместе со столь возвышенными людьми, которые в своем познании настолько преисполнились.
В любом случае, ошибка интервьюера - это плохой знак, на котором мелким шрифтом написано, что работы вам не видать.
👍5🔥1