C++ Embedded
425 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
Media is too big
VIEW IN TELEGRAM
Не могу не вспомнить нашего парня, Антона Полухина, который в 2016 году на CppCon рассказывал про такие штуки, от которых шевелились волосы во всяких местах. Да, он выжимает из плюсов все соки, вертит ими как хочет. В данном докладе Антон рассказывает о рефлексии, ведь это слабое место языка. Оригинальный подход к проблеме меня в свое время поразил до глубины мозга. Главное же, это приемы, которые были использованы: можно получить некоторую информацию о структуре POD, исследуя ее инициализацию в фигурных скобках. Комбинируя этот трюк с вариативными шаблонами, функциями constexpr, операторами неявного преобразования, SFINAE, decltype и целочисленными константами, мы можем подсчитывать поля структуры и даже определять тип каждого поля.
Что здесь:
- What's in the header
- Idea for counting fields
- One more idea
- Tacking care of pointers
- Nested structures and classes
- Compile times
- Structure binding for greater good
👍1
std::optional
Довольно милая концепция, даже странно, почему товарищи из IAR не потрудились ее реализовать. Это не так уж и сложно. Возможно, есть причины, по которым этот концепт обошли стороной. Не все так просто с optional. Корректен ли такой код? (для gcc)
std::optional<std::uint8_t> x{100};
В общем-то, да. Константа 100 имеет тип int, но ее значение известно и попадает в диапазон значений типа uint8_t, число без проблем конвертируется.
Возьмем более очевидную структуру:
struct UChar { std::uint8_t x; };
UChar c {100};
Предупреждений нет, работает корректно.
UChar c {100};
error: narrowing conversion of '1000' from 'int' to 'uint8_t' {aka 'unsigned char'} [-Wnarrowing]
А вот тут начались проблемы, 1000 ну никак не влезает в диапазон значений uint8_t, получаем предупреждение об этом, что справедливо.
А что optional?
std::optional<std::uint8_t> x{1000};
Нет ошибок, нет предупреждений. Значение внутри - 232, а не 1000. Вот такое коварство, теперь живите с этим.
std::launder
Новая стандартная функция, назначение которой никто не знает. Наверное поэтому ее и не реализовали в IAR. Или нет?
std::launder получает указатель на объект, расположенный по адресу, переданному через аргумент. То есть, передаем адрес и получаем тот же адрес.
Но есть нюанс! Эта функция предотвращает оптимизацию. Компиляторы стали слишком умные и могут запросто выкинуть какой-нибудь фортель в особых случаях.
В GCC есть тест, который хорошо иллюстрирует такой казус.
struct Test5A { const int x; };
struct Test5B { Test5A a; };
static void test5f(Test5B &b) {
new (&b.a) Test5A{666};
}
void Test() {
Test5B b{{42}};
test5f(b);
assert(std::launder(&b.a)->x, 42);
}

Если скомпилировать Debug сборку даже в GCC будет работать корректно, но если включить агрессивную оптимизацию, то результат может удивить.
Так вот, в IAR такой проблемы вообще нет. Нет оптимизаций - нет проблем. launder не нужен.
👍2
Memory-Mapped Devices as Objects - Dan Saks
Отличный доклад попался мне на CppCon 2020: "Memory-Mapped Devices as Objects". Автор этого замечательного опуса, Dan Saks еще и целый президент компании Saks & Associates, которая предлагает обучение и консультации по языкам C и C++ и их использованию в разработке встраиваемых систем. Дэн раньше вел колонку на сайте embedded.com и, вообще, делает много чего интересного.
Как вы знаете, современные процессоры взаимодействуют с внешними устройствами или периферией через регистры устройств с отображением в памяти.
Можно, конечно, использовать сишний классический подход для работы с ними. Это до сих пор широко распространенная практика. Взять хоть ST-HAL библиотеку. Но это чревато бессонными ночами, проведенными в обнимку с отладчиком.
Поэтому здесь вам покажут как C++ мастерски справляется с регистрами устройств в памяти. Вы можете использовать базовые классы и шаблоны для выявления общих черт между различными устройствами, использующими схожее размещение регистров. Упакуем отображаемые в память устройства как легковесные объекты класса, которые легко использовать правильно и трудно использовать неправильно!
Честно скажу, если вы хотите переиграть и уничтожить наконец-то богомерзкую ST-HAL, то лучше посмотреть и понять как делать правильно.
Ключевые моменты:
- Device registers
- Control Registers
- A Typical Address Space
- GNU Linker
- Reference Placement
- Negative Consequence
- UART Members
- Recap
👍2
Media is too big
VIEW IN TELEGRAM
Memory-Mapped Devices as Objects - Dan Saks
Видео
std::launder
Нет, launder таки нужен? Для переносимости кода? Хм... ну, может быть.
Есть реализация этой функции, которая будет работать как для IAR, так и для GCC.
template <typename T>
inline auto launder(T *p) -> T * {
asm("" : "+r"(p));
return p;
}

Да, малюсенький грязненький хак, ассемблерная вставка. Она не делает ничего, но декларирует выходной операнд p с параметрами "+r".
"r" - это регистр, а "+" - использование операнда для чтения и записи. Компилятор не посмеет оптимизировать такую страшную конструкцию.
👍2
Media is too big
VIEW IN TELEGRAM
Антон Квятковский — Type loopholes in C++: Убербаг уровня стандарта
Наши товарищи тоже делают отличные доклады. Антон Квятковский сделал страшное открытие глубоко в недрах метапрограммирования С++. Настолько пугающее, что и сейчас мне даже вышептать боязно.
А так, там про механизмы добавления и изменения глобального состояния на этапе компиляции, примерами практического применения этих техник. Вроде как получение списка типов агрегата, имплементируем constexpr-счётчик и другие фичи, которые невозможно реализовать без этой лазейки. Смотреть можно только тем, кто видел кое-какое метапрограммирование. А то у вас лопнет глаз. Или оба.
👍2
Media is too big
VIEW IN TELEGRAM
Non-conforming C++ the Secrets the Committee Is Hiding From You - Miro Knejp
Лучший доклад CppCon в 2019 году для меня, это, конечно, срывающий покровы и обличающий заговоры Комитета Миро. Мало того, что он рассказывает про сверхспособности goto, Элвиса и всякое такое, расширяющее сознание компилятора, но у чувака есть шапочка из фольги!
Особенно полезно видео будет тем, кто использует GCC. Вы узнаете всякие трюки, которые не входят в стандарт. Те, кто использует IAR, извините, вам не спастись.
Спешите видеть, пока агенты Комитета не удалили этот доклад вместе с автором.
Что интересного:
- Unnamed Structure and Union Fields
- Conditionals with Omitted Operands
- The Elvis Operator
- Designated Initializers
- Designated Array Initializers
- This is why the Committee Hates you
- Flexible Array Member
- Labels as Values
- Portability
👍2
std::launder
Время интереснейших историй! Изначально реализация launder-а в GCC была весьма скучна и тривиальна:
template<typename _Tp>
constexpr _Tp *launder(_Tp *__p) noexcept {
return __p;
}

А чего долго думать? Он же ничего не делает, тунеядец! Только оптимизации распугивает, будто прикормленную рыбу брошенный в водоем пьяный тимлид.
Пока вдруг в обсуждение не ворвался непоследний разработчик Clang-а Ричард Смит и не вывалил на стол свой пример.
struct A {
virtual int f();
};

struct B : A {
virtual int f() { new (this) A; return 1; }
};

int A::f() { new (this) B; return 2; }

int h() {
A a;
int n = a.f();
int m = std::launder(&a)->f();
return n + m;
}

При оптимизации GCC просто вернет 4 как результат вызова функции h(). Компилятор весьма опрометчиво решит, что знает динамический тип объекта и тут же применит оптимизацию "девиртуализация". Т.е. заменит вызовы виртуальных функций на прямые вызовы. Вот тут он и сядет в лужу, поскольку после первого вызова динамический тип меняется.
Резкий как "Нате!" Маяковского, звонкой пощечиной разнесся сей опус в узких кругах, заставив униженных разработчиков GCC изменить реализацию launder-а. Теперь вместо ничегонеделания в стандартной функции вызывается встроенная функция __builtin_launder. Собирается все тоже корректно, h() вернет 3, как и положено.
А предложенный пример, наделавший так много боли, остался в стандартных тестах GCC где-то в районе gcc/testsuite/g++.dg/cpp1z/launder1.C. Сам видел!
👍2
IAR clock
Как на какой-нибудь платке stm32 получить значение времени? Если, например, надо длительность выполнения тестов измерить на стандартном симуляторе IAR (GTEST по умолчанию делает, кстати).
Не вопрос! С++ предоставляет замечательный chrono в стандартной библиотеке, где есть такие удобные вещи, как steady_clock и system_clock. Первые часы дают нам время с последнего ребута, второй - системное время. Забавно то, что для IAR разницы нет. Тут steady_clock тупо наследуется от system_clock, вот и вся недолга.
Функция now(), хоть и не прямо, но вызsвает функцию clock(). А что говорит нам IAR в своих документах о стандартных функциях времени? Правильно, с подключенным отладчиком все будет работать из коробки.
Не поверим документам и глянем, что там насобирает этот негодный компилятор:
clock:
movs r1, #0
movs r0, #16
bkpt #0xab

Хм, приемлемо. В регистр r1 заносится значение 0, в регистр r0 кладем 16 и вызываем брейкпоинт. Грубо говоря, такой брейкпоинт будет проигнорирован реальным железом, но дебаггер поймет, что обращаются к нему. Это т.н. semihosting. Значение 16 мы положили в r0 не просто так, это аргумент для отладчика, указывающий на тип операции:
Time operations
SYS_CLOCK (0x10) <-- вот оно!
SYS_ELAPSED (0x30)
SYS_TICKFREQ (0x31)
SYS_TIME (0x11)

Остальные параметры должны были быть в блоке памяти, указатель на который лежит в r1. К счастью, там ноль.
Добрый отладчик же в ответ нам наложит в регистр r0 значение времени. В симуляторе это тоже работает. Перестанет работать без отладчика, вот это будет большая проблема. В таком случае IAR рекомендует честно реализовать свою честную функцию clock(). Тут уж извините, куда деваться.
👍3
Media is too big
VIEW IN TELEGRAM
Handling a Family of Hardware Devices with a Single Implementation - Ben Saks
Имя Ben Saks может показаться вам смутно знакомым, и это неспроста! Компания Saks & Associates вновь радует нас своим шаблонным представлением встроенного программного обеспечения для процессоров больших и маленьких. На этот раз главный инженер компании и по совместительству человек с фамилией Saks, разовьет прошлогоднюю тему про "Memory-Mapped Devices as Objects" и покажет способы использования перегрузки операторов, типов перечисления и других возможностей C++ для создания интерфейсов, защищающих аппаратные регистры от неправильного использования. Несмотря на явное кумовство в компании, доклад довольно интересный. Бен разберет реальный пример создания безопасного интерфейса для 8-битных и 16-битных аппаратных таймеров. Шаблоны будут, конечно, не сомневайтесь.
- Device Registers
- Memory Mapped Hardware
- Six Programmable Timers
- Interrupt Mask Register
- Clock Source Enumeration
- Timer Traits
👍3
[[gnu::may_alias]]
Это что за атрибут? Всегда полезно помнить, что оптимизации полны коварства. Чтоб упростить себе работу всякий ленивый компилятор предполагает, что указатели или ссылки на объекты разного типа не могут вести на один и тот же участок памяти. А что? Он не обязан после смены вкалывать! Это закон strict-aliasing, если нарушаете, то получите UB.
int i = 42;
float *pa = (float *) &i;
*pa = 0.F;

Здесь, например, мы злонамеренно конвертируем указатель int* во float*, потом присваиваем нулевое значение по этому указателю. Что же будет со значением переменной i? Без оптимизаций значение изменится как мы и предполагали в 0. Теоретически, при достаточно агрессивных оптимизациях компилятор не поверит, что значение i изменилось. Нет, указатели же разного типа, значит и меняться будет что угодно, но только не i, и будем считать его значение равным 42. На практике же, последние GCC обмануть не удалось, этот случай там выполняется интуитивно корректно, хоть это все равно UB.
struct float_w { float x; };

int foo( float_w *f, int *i ) {
*i = 1;
f->x = 0.f;
return *i;
}

int main() {
int x = 0;
return foo(reinterpret_cast<float_w*>(&x), &x);
}

Здесь более интересный случай, когда UB приведет к разному результату. На GCC точно. Со включенными оптимизациями код возврата равен 1. Компилятор ясно видит, что по адресу i присвоили значение, которое и стоит вернуть, не важно, что там делали с другими адресами. Это про этих там.
struct [[gnu::may_alias]] float_w { float x; };

Все меняется, если применить наш атрибут. Мы как бы говорим компилятору, что это хоть float_w тип, но содержаться в нем может все, что угодно! Не руби с плеча. В общем, применяем -fno-strict-aliasing для отдельной структуры. Лучше не злоупотреблять, преобразование типов приведением через указатель всегда было опасной штукой.
👍3
Media is too big
VIEW IN TELEGRAM
Timur Doumler — Type punning in modern C++ (C++ Russia 2019 PITER)
В далеком 2019 году этот доклад открыл мне глаза на преобразования типов. Вроде бы дело нехитрое, из float получить int.
int i = reinterpret_cast<int>(float_value);

Я и сам не гнушался такое писать... давно...
unit {
float float_value;
int int_value;
}

Метод для эстетов, знающих толк в извращениях. Это может работать, но Тимур говорит, что это UB, это опасно, и кругом враги! И я ему верю, как и стандарту, который он цитирует.
Проблемы, о которых нельзя молчать:
- aliasing rules
- object lifetime rules
- alignment rules
- rules of valid value representation
Есть ли путь без боли и UB? Есть!
std::memcpy(&int_value, &float_value, sizeof(int));

Немного больше повезет тем, кто уже использует c++20
std::bit_cast<int>(float_value);

Курьез: std::complex<float> может быть преобразован во float(&)[2] без нареканий. Работает только с std типом, иначе только pointer-interconvertible. Не забываем, что std безгрешен!
👍3
std::function
Впервые взглянув на реализацию функции в стандартной библиотеке gcc, я был немало удивлен отсутствию reinterpret_cast внутри. Мои наивные реализации упирались в необходимость превращать некий кусок памяти в вызываемый объект, и тут уж никак без противоестественных преобразований типов. Ведь в std::function может быть записаны и функции, и функторы, и методы, и все это с разными возвращаемыми типами, разными наборами аргументов. Почему же gcc не испытывает никаких неудобств? Заглянем в сердце тьмы.
В том черном сердце function лежит не смерть кощеева, но очень интересный объект: _Any_data. Несложно предположить по названию, что он может содержать любые данные.
union [[gnu::may_alias]] _Any_data {
void* _M_access() { return &_M_pod_data[0]; }
const void* _M_access() const { return &_M_pod_data[0]; }

template<typename _Tp>
_Tp& _M_access() { return *static_cast<_Tp*>(_M_access()); }

template<typename _Tp>
const _Tp& _M_access() const { return *static_cast<const _Tp*>(_M_access()); }

_Nocopy_types _M_unused;
char _M_pod_data[sizeof(_Nocopy_types)];
};

Несложно догадаться, что все дело в методе _M_access, ведь других здесь и нет. Нешаблонный метод возвращает void* указатель на _M_pod_data. Шаблонный метод возвратит ссылку на любой тип, который мы захотим. Просто и без затей разыменует преобразованный указатель void*, полученный из нешаблонного метода. Если бы мы попытались сделать такое преобразование
static_cast<_Tp*>(_M_pod_data)

то компилятор бы громко негодовал. Интересное кино, чтоб преобразовать void* в любой другой указатель и обратно хватит и static_cast-а. Это пугает и завораживает. Главное, AUTOSAR не имеет ничего против.
_Nocopy_types задает размер и выравнивание нашего волшебного сундучка.
class _Undefined_class;
union _Nocopy_types {
void* _M_object;
const void* _M_const_object;
void (*_M_function_pointer)();
void (_Undefined_class::*_M_member_pointer)();
};

Тут все просто. Наш замечательный тип должен быть готов вместить в себя указатель на объект, на функцию, на метод класса. Да, хоть и _Undefined_class класса никогда не существовало, не существует и, весьма вероятно, что и не будет существовать, размер указателя мы все равно сможем определить. Скорее всего, это будет 2 * sizeof(void*). Ежели желаете упихнуть туда коленом что-то помассивнее, то std::function нисколько не смутившись динамически выделит память для вашего корпулентного функторчика. Вот поэтому лучше не использовать стандартные функции в совсем уж мелком embedded.
👍3
EBO
Гусары, молчать! Это не то, что вы подумали, это просто Empty Base Optimization идиома. Она же Empty Base Class Optimization. Она же Empty Member Optimization.
Не секрет, что некоторые классы вполне себе могут обойтись и без членов. Не надо смеяться! Вот какой-нибудь внешний деструктор или аллокатор, а может и std::iterator у которых за душой ничего нет, только вся ценность их в типе и поведении, которое они определяют.
Размер пустого класса в C++ равен 1 байту. Ничего странного в этом нет, каждый объект должен иметь свой адрес. А то пойдут коллекторы в гости к моему соседу, а попадут ко мне. Было бы не слишком приятно. Ладно, это к делу не относится. Что будет, если мы хотим включить пустой класс в другой объект?
struct Empty {};
struct Pair {
uint32_t i;
Empty e;
};

Размер Pair равен аж 8 байтам! Это форменное расточительство, ведь Empty объект пуст как голова студента после сессии.
Как говорят великие гуманитарные мыслители современности, выход есть!
struct Pair : Empty {
uint32_t i;
};

Если наследоваться от пустого класса, то размер не увеличивается, он равен 4 байтам. Пустой класс как член не очень полезен, а наследуясь от него, мы "забираем" именно его поведенческие особенности.
Есть нюанс! Иногда такая оптимизация невозможна, если первый член класса того же или унаследованного типа. Почему нет? А вот представьте пикантную ситуацию:
struct Pair : Empty {
Empty e;
Empty *get() { return this; }
};

Если оптимизация сработает обычным образом, то this будет указывать на e. Все смешается как в доме Облонских, и будет не разобрать на что же мы указываем. В общем, не будем мы такое непотребство оптимизировать! Вот если e не первый, то никакой неоднозначности нет, можно оптимизировать будто систему здравоохранения.
👍3
[[no_unique_address]]
Это что за атрибут?
В С++20 появился замечательный новый атрибут, который позволяет членам классов и структур теряться на фоне своих коллег. Нехорошо звучит. Атрибут позволяет делить один и тот же адрес со своими соседями. Впрочем, это не сработает для статических членов класса и битовых полей. Такая же идея, что и в EBO оптимизации: если у нас есть пустой класс, то не стоит тратиться на его размещение. Пусть спит валетом с бабушкой на старом диване. Рассмотрим наш "классический" пример:
struct Empty {};
struct Pair {
[[no_unique_address]] Empty e;
uint32_t i;
};

Как мы помним, без волшебного атрибута размер Pair был равен 8 байтам, а используя последние достижения стандартизаторской мысли, ужмем нашу пятилетку структуру в 4 байта.
Очень удобно применять в каких-нибудь шаблонах, если предполагаете, что работать придется с пустым типом.
template <class T1, class T2>
struct TwoTypes {
uint8_t i;
[[no_unique_address]] T1 e1;
[[no_unique_address]] T2 e2;
};

Как и в EBO, тут действуют похожие правила: неоднозначности - нет!
Размер TwoTypes<Empty, uint8_t> будет равен двум байтам. Такой же размер будет и у TwoTypes<Empty, Empty>. Потому как два члена класса одинакового пустого типа делить между собой один адрес не могут. Нормально сработает только для разных пустых типов.
👍3
Media is too big
VIEW IN TELEGRAM
Implementing static_vector: How Hard Could it Be? - David Stone - CppCon 2021
Какой разработчик не мечтает изобрести велосипед? Только тот, что хочет создать свой класс контейнера. Это не так уж и просто, на самом-то деле. Необходимо учесть множество деталей, в которых спрятался сам Люцифер. Вот в своем докладе David Stone и расскажет нам про некоторые подводные камни и покажет где зарыта собака. А, да, про контейнеростроение тоже просветит: как сделать static_vector более constexpr, как обуздать привычный интерфейс std::vector и другие разные интересные факты из жизни контейнеров после прихода c++20.
Автор завалит вас тоннами кода, разбираться в котором будет одно удовольствие, все равно что есть шаурму пытаясь понять из чего она сделана. Обязательно нужно вдумчиво посмотреть, если хотите сделать вектор имени себя. Может быть, вы изгоните эти тщеславные мысли с позором опосля.
👍3
confuse the constructors
Ничто так не радует глаз, как стопроцентное покрытие кода. Поэтому мой глаз не радуется, а дергается. После всех написанных тестов и бессонных ночей покрытие не 100%?! Да как такое вообще возможно?!
_ZN5TestAC1Ei
Вот этот метод портит нам жизнь! Откройся, царица полей!
> c++filt _ZN5TestAC1Ei
TestA::TestA(int)
Ба! Да это же конструктор. Странно, нельзя же забыть вызвать конструктор? Давайте посмотрим наш простой и беспощадно бесполезный класс:
class TestA final {
public:
TestA(int i) : i_ {i} {}
~TestA() = default;
int get() const { return i_; }
private:
int i_;
};
Изучая символы внутри скомпилированного файла изумляемся наличию аж нескольких конструкторов:
00008238  w    F .text  00000034 _ZN5TestAC2Ei
00008238 w F .text 00000034 _ZN5TestAC1Ei
...
Имена похожи, отличаются только метками после имени: С1 и С2. Что же, вдумчивое чтение документа Itanium C++ ABI может помочь.
C1 - complete object constructor
C2 - base object constructor
C3 - complete object allocating constructor
Конструктор полного объекта (С1) вызывается, когда нужно сконструировать весь объект целиком. Это как раз наш обычный скучный конструктор. Конструктор базового объекта (С2) используется конструктором производного класса, если объект создается как базовая часть наследника.
Вообще-то, в подавляющем большинстве случаев нет никакой разницы между этими конструкторами: базовый объект создается теми же методами, что и очень полный объект.
Разница будет только в случае виртуального наследования. Вот тут в дело вступает тяжелая артиллерия в виде virtual-table table (VTT). Что-то вроде таблицы для виртуальных таблиц, крайне полезная вещь для построения пятиюродных внучатых племянников, ой, т.е. виртуальных наследников. Не вдаваясь в виртуальные дебри, которые и так все знают, подытожим. Полный конструктор объектов (C1) самоотверженно берет на себя всю работу по построению всего, словно заправский передовик производства: и выборку VTT, и вызов всех базовых построений. Конструктор базового объекта (C2) заботится непосредственно только о самом объекте. Экий тунеядец!
У нашего класса нет никакого наследования, его конструктор не должен ничего знать о VTT. Поэтому разницы между полным и худым конструктором нет.
Дальше все будет зависеть от реализации компилятора. Может быть он не будет создавать лишний конструктор, может быть он создаст две одинаковые копии. Возможно один конструктор будет ссылаться на другой.
.thumb_set TestA::TestA(int) [complete object constructor],TestA::TestA(int) [base object constructor]
или
.set    _ZN5TestAC1Ei,_ZN5TestAC2Ei
Директивы thumb_set или set, как легко догадаться, говорят прямо о тождественности полного и неполного конструктора. Так что, не стоит удивляться, если найдете неиспользуемые конструкторы у себя. Это блажь компилятора, а не ваша вина.
Мы что-то упустили? А, точно! Полный конструктор размещения объектов (С3), вроде должен как-то задействовать оператор new для размещения объекта, но точно никто не знает, даже GCC. Поэтому никогда его не генерирует.
👍3
confuse the destructors
Что же, наш любимый компилятор может генерировать не только интересные конструкторы, но и головную боль в виде разных деструкторов. При подсчете процента покрытия кода будет сюрприз.
Посмотрим на символы деструктора для TestA:
00008504  w    F .text  00000034 _ZN5TestAD1Ev
00008504 w F .text 00000034 _ZN5TestAD2Ev
00008538 w F .text 00000038 _ZN5TestAD0Ev

Полный набор, от D0 до D2. Снова обратимся к документу Itanium C++ ABI, чтоб распознать наш деструктивный зоопарк.
D0 - deleting destructor
D1 - complete object destructor
D2 - base object destructor
Деструктор базового объекта (D2) по аналогии с соответствующим конструктором, как всегда, заботится только о себе. Делает самую простою работу, т.е. уничтожает все нестатические члены и невиртуальные базовые объекты.
Для дополнительной иллюстрации построим небольшую иерархию классов.
struct TestA {
TestA(int i) : i_ {i} {}
virtual ~TestA() = default;
int i_;
};

struct TestB : virtual public TestA {
TestB(int i) : TestA{i} {}
~TestB() override = default;
};

struct TestC : virtual public TestA, virtual public TestB {
TestC(int i) : TestA{i}, TestB{i} {}
~TestC() override = default;
};

Здесь TestC точно будет иметь в своем составе виртуальные базовые объекты. Значит, ему точно потребуется деструктор D1 (complete object destructor).
Деструктор полного объекта (D1) перерабатывает, как и одноименный конструктор: не только уничтожает объект, но и разрушает еще и виртуальные базовые объекты (вызывает их базовые деструкторы).
Можно легко увидеть вызовы других базовых деструкторов внутри.
TestC::~TestC() [complete object destructor]:
push {r7, lr}
...
bl TestB::~TestB() [base object destructor]
...
bl TestA::~TestA() [base object destructor]
...

Удаляющий деструктор (D0) не только уничтожает весь объект со всеми базами, но и высвобождает память, которую он занимал.
TestC::~TestC() [deleting destructor]:
push {r7, lr}
...
bl TestC::~TestC() [complete object destructor]
...
bl operator delete(void*, unsigned int)
...

Если у объекта нет никаких виртуальных базовых классов, то базовый деструктор можно объединить с деструктором полного объекта.
.thumb_set TestA::~TestA() [complete object destructor],TestA::~TestA() [base object destructor]

Однако, удаляющий конструктор всегда отличается, и инструмент подсчета покрытия кода принимает его за самостоятельную функцию. Надо еще очень постараться, чтоб вызвать именно его. Ведь для его вызова надо явно удалить объект через оператор delete, а для удаления объекта подобным образом надо явно создать объект через оператор new. Сомневаюсь, что кто-то горит желанием динамически выделять память после main, особенно когда это явно запрещено правилами SIL. Поэтому в покрытии кода намечается пробелы. Можно, конечно, написать специальные тесты, которые будут создавать объекты и тут же удалять их, но это немыслимое извращение.
👍4
С чего начинается прошивка. isr_vector.
Все знают, что жизнь начинается с main, но самое интересное начинается со старта микроконтроллера. Рассмотрим, что происходит после старта с некой абстрактной платкой на базе stm32l452. Очнувшись, процессор судорожно ищет в начале своей памяти (0x08000000) указатель на стек (SP). Загрузив его и немного успокоившись, осматривает вектор прерываний дальше. Следующим пунктом идет адрес функции, которую наш контроллер испытывает непреодолимое желание безотлагательно исполнить. Все просто, осталось просто подсунуть нужные данные, чтоб исполнилось именно наше желание, т.е. main.
Попробуем собрать с помощью GCC самую маленькую и бесполезную прошивку. Пусть содержимое файла dummy.cpp будет таким:
int main () {
return 0;
}
Посмотрим, что мы можем слепить из этого
arm-none-eabi-g++ dummy.cpp --specs=nosys.specs -fno-exceptions -fno-rtti -O0 --std=c++17 -o dummy
arm-none-eabi-objcopy -O binary dummy dummy.bin
Ошибок нет! Мы получили бинарник, который заведет наш контроллер? Не совсем. Заглянем в начало сгенерированного файла
00000000  0d c0 a0 e1 f8 df 2d e9
Если бы я был процессором, то с такой прошивкой в регистр SP загрузил бы 0xe1a0c00d, а обработчик сброса бы пошел искать по адресу 0xe92ddff8. Думаю, ничего хорошего из этого не вышло бы.
Да, все собралось без ошибок, но процессор просто не знает как попасть в наш прекрасный main. Собственно, не хватает того, что и делает прошивку прошивкой - вектора прерываний. Процессор для обработки прерывания обращается в соответствующую ячейку памяти в начале своей постоянной памяти (хотя вектор потом можно переложить в другое место). Как добавить этот кусок в начало? Использовать скрипт для линковки! Еще любезно предоставленный вендором код для инициализации нашей платы.
На будущее, добавим функцию SystemInit, ее потребует код обработчика сброса.
extern "C" {
void SystemInit(void) {}
}
Нам пришлось обернуть функцию в extern т.к. собираем мы этот код как плюсовый.
arm-none-eabi-g++ dummy.cpp startup_stm32l452xx.s --specs=nosys.specs -fno-exceptions -fno-rtti -T STM32L452RETx_FLASH.ld -O0 --std=c++17 -o dummy
В итоге мы получили вменяемый адрес указателя стека и начального обработчика.
00000000  00 80 02 20 7d 03 00 08
SP - 0x20028000; обработчик должен располагаться по адресу 0x0800037d, посмотрим, что там на самом деле.
0800037c <Reset_Handler>:
Тут прописан Reset_Handler и наш дорогой обработчик по совместительству. Наипервейшая функция, вызываемая после включения процессора.
Природа интересно устроена, как говорилось в одном анекдоте, глазки у кошки именно там, где дырочки в шкурке. Скрипт линкера задает описание секции .isr_vector, где конкретно она будет располагаться во flash-памяти (в самом начале же, там и комментарий есть на тему);
  /* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(8);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(8);
} >FLASH
startup файл описывает содержание секции, ее богатый внутренний мир.
    .section        .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors

g_pfnVectors:
.word _estack
.word Reset_Handler
Флаг секции "а" намекает, что эта секция может загружаться в память; progbits говорит о том, что в секции данные и/или инструкции для инициализации.
_estack - конец оперативной памяти, как можно определить по названию. Естественно, этот феномен описан в скрипте.
_estack = 0x20028000;    /* end of RAM */
Таким вот нехитрым способом можно заставить микроконтроллер работать. То есть, все же добраться до main и уже там где-то споткнуться о какой-нибудь нелепый баг.
👍4