C++ Embedded
425 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
std::is_constructible & IAR. SFINAE
– Скажите, – спросили коллеги жалобно, – а SFINAE не может спасти гигантов мысли из IAR?
Совсем же просто написать метафункцию для выделения каких-то свойств объекта, если под рукой есть std::void_t из с++17. std::is_costructible могла бы выглядеть в нашем исполнении как-то так:
template <class T, class A, class = void>
struct is_constructible_custom : std::false_type {};
template <class T, class A>
struct is_constructible_custom<T, A, std::void_t<decltype(T{std::declval<A>()})>> : std::true_type {};
Здесь, если объект типа T никак невозможно создать, передавая ему аргумент типа A, то частичная специализация не сработает, и компилятор остановится на более общем и корректном варианте.
Да, метафункция работает только с одним аргументом, но это же для примера! Проблема в том, что просто расширить функцию через шаблон с переменным количеством аргументов не получится из-за параметра по умолчанию в конце. Компилятор не поймет, то ли вы ему тип аргумента подсовываете, то ли насильно перегружаете последний параметр.
Можно, конечно, объединить типы в коллекцию std::tuple и родить что-то вроде
template <class T, class S, class = void>
struct is_constructible_impl : std::false_type {};
template <class T, class ... A>
struct is_constructible_impl<T, std::tuple<A...>, std::void_t<decltype(T{std::declval<A>()...})>> : std::true_type {};
template <class T, class ... Args>
struct is_constructible_custom { static constexpr bool value {is_constructible_impl<T, std::tuple<Args...>>::value}; };
но это уж, сватушка, лишнее и к делу не относится. Главное, что это бы сработало для GCC, но вдруг совершенно не пригодно для IAR, когда дело касается проклятой std::function.
static_assert (is_constructible_custom<std::function<void()>, int>::value);  // sad but true
Плевать хотел наш любимый компилятор на все наши танцы вокруг. А все потому, что decltype(std::function<void()>{1}) для IAR существует!
Не видит он никакой крамолы в этом выражении и выдает std::function<void()> как результирующий тип. То, что должно сломаться при сборке и подтолкнуть механизм к правильному решению, не срабатывает.
Терять нечего, и нам доступно вероломство. Попробуем поправить поведение прямо в пространстве имен std:
namespace std {
template<class _Ty, class Signarure, class = void>
struct is_callable : std::false_type{};
template<class _Ty, class R, class ... Args>
struct is_callable<_Ty, R(Args...), std::void_t<decltype(std::invoke(std::declval<_Ty>(), std::declval<Args>()...))>> : std::true_type{};

template<class F, class Arg>
struct is_constructible<std::function<F>, Arg> { static constexpr bool value {is_callable<Arg, F>::value}; };
template<class F, class ... Arg>
struct is_constructible<std::function<F>, Arg...> : false_type {};
} // namespace std
Почему нет? Самый распространенный случай - передача в конструктор std::function объекта, который должен быть Callable, т.е. его можно вызвать как функцию. Вот это свойство аргумента мы и проверим, для чего возьмем список типов аргументов из сигнатуры std::function и примерим его к вызову предполагаемого объекта Arg. Передавать множество объектов в стандартную функцию почти не имеет смысла. На самом деле там есть варианты, аллокатор какой-нибудь тиснуть, но это редкости и тем более в c++17 такое уже убрали.
Кому там код пахнет? Он свежий. Делать, так, конечно не рекомендуется, но...
Что ж вы ругаетесь, дьяволы? Иль я не сын страны? Каждый из нас закладывал в namespace std чего-нибудь.
👍6🤔1
Продолжение истории POD
Внимательный читатель спросит меня, почему я умолчал о знаменитой проблеме 383? (383. Is a class with a declared but not defined destructor a POD?)
Класс с объявленным, но не определенным деструктором тоже будет POD?
Проблему обнаружил явно какой-то душнила, не будем показывать пальцем, хотя это был Gennaro Prota в 2002 году. Да, в определении POD, согласно с++98, действительно говорится об определяемых пользователем деструкторе и копирующем операторе присваивания, а не об объявленных пользователем!
Например:
struct Foo {
int x;
~Foo() {}
Foo& operator= (Foo const &fraction) { ... }
};
здесь класс Foo теряет звание POD, а если реализация будет отсутствовать:
struct Foo {
int x;
~Foo();
Foo& operator= (Foo const &fraction);
};
то это уже совсем другое дело! Правда, объект такого типа создать не удастся, но в метафункциях тип может быть использован, поэтому ошибка невероятная серьезная. Настолько серьезная, что голосовали по исправлению определения POD аж в 2004 году, когда стандарт с++03 уже был сверстан и принят, зато новое определение сохраняется в черновиках стандарта вплоть до 2006 года. Дальше оно было творчески переработано с подачи другого зануды...
👍5
Продолжение истории POD
В 2005 году суетливый Alisdair Meredith озадачил сообщество главным вопросом философии С++: а чего у вас тут везде struct, а не class?
(538. Definition and usage of structure, POD-struct, POD-union, and POD class)
Это плюсы, и структурам тут не место, их вообще не существует же. Пора забыть свои позорные сишные корни. Кроме концептуальных возражений были и вполне конкретные претензии. Термин POD-struct, судя по всему, введен для исключения объединений, однако само определение агрегатного класса эти объедения допускает. Вышло небольшое недопонимание.
Термин множество раз может сбить с толку: "А struct здесь это только структура или класс тоже имеется в виду?". Часто POD-struct и POD-union упоминаются вместе, почему бы не заменить их более емким и православным термином POD class? Автор предложил исправить определение следующим образом:
POD class - это агрегатный класс, который не имеет объявленных пользователем оператора присваивания копированием и деструктора и не содержит нестатических членов не POD-структур или не POD-объединений (или массивов подобных типов) или ссылок.
Про POD-struct благодетель тоже не забыл. Указал, что это может быть как и struct, так и нормальный class.
Правки были приняты голосованием в 2007 году, тогда же можно увидеть исправления в черновике стандарта.
Все это были очень важные и душные изменения, но народ потихоньку уже начинал роптать, к концепции POD появлялись более серьезные вопросы. В воздухе витает идея пересмотра самого понятия. A suivre.
👍7
std::function & IAR
Мало кто знает, но IAR, на самом-то деле, это румынская авиастроительная компания (Industria Aeronautică Română). Поэтому соответствующий компилятор и летает так же грациозно, как и пущенный с 17-го этажа самоучитель "С++ за 21 день". Следом бы стоило отправить и хозяина этой книги, некоего разработчика стандартной библиотеки по версии IAR.
Как вы уже догадались по расходящимся лучам благодарности, странное поведение метафункций (std::is_constructible<std::function<void()>, int>::value == true и
decltype(std::function<void()>{1}) == std::function<void()>) объясняется исключительно недостатками реализации местной версии std::function.
Среди прочего в описании класса присутствует такой чудесный шаблонный конструктор
template<class _Fx>
function(_Fx _Func) { // construct wrapper holding copy of _Func
this->_Reset(std::move(_Func));
}
который очень красноречиво говорит о своей всеядности всем метафункциям, что готовы слушать. Здесь заявлено, что конструктор принимает любой тип в качестве параметра, никаких ограничений в виде enable_if и прочих наложенных на тип рестрикций нет и в помине. Тут бы и GCC опозорился. Только представьте себя на его месте, когда видите такой вот тип:
class StupidFunction : public std::function<void()> {
public:
template<class _Fx>
StupidFunction(_Fx _Func) : std::function<void()>(_Func) {}
};
Задумка понятна, мы передаем в конструкторе некую вызываемую сущность, и если использовать класс по назначению, то проблем не будет. Тем не менее, явных ограничений на тип нет, и это собьет компилятор с толку при первой же нештатной ситуации. Видимо, делает поверхностные выводы только по формальным признакам.
static_assert(std::is_constructible<StupidFunction, int>::value)  // OK
using Type = decltype(StupidFunction{1}); // OK
Ну вот, говорит, что проблем нет, но в этом не больше правды, чем в утверждении, что питон - лучший язык программирования.
При попытке создать объект StupidFunction
StupidFunction f{1};
мы узнаем много нового о своих интеллектуальных способностях
error: no matching function for call to 'std::function<void()>::function(int&)'
Видимо, не стоит так писать шаблоны для стандартной библиотеки, особенно когда продаешь это за деньги.
👍7🤔2
Пересмотр POD
В 2006 году Matt Austern поднимает проблему 568 (Definition of POD is too strict). Определение POD слишком строгое. Примечательно, что это формирование уже существенных претензий к определению POD. В том же году появился документ POD's Revisited как ответ на накопившиеся вопросы. Стало быть, наши старые добрые типы, перекочевавшие прямиком из Си, ждала политика ревизионизма.
Итак, чем же плох POD? Он имеет много незаслуженных привилегий, таких как гарантии совместимости по компоновке и побайтного копирования.
В первом случае речь идет о главе 9.2 Class members, стих 14:
Две POD-структуры совместимы по компоновке, если у них одинаковое число нестатических членов, соответствующие типы которых совместимы по компоновке.
Например, вот эти две структуры:
struct Foo {
int x;
char y;
};
struct Bar {
const int u;
volatile char v;
};
Если отбросить квалификаторы, то типы идентичны. Собери такие структуры в union, и легальный доступ к членам класса можно получить как со стороны Foo, так и со стороны Bar.
Гарантии побайтного копирования плавно вытекают из главы 3.9 Types стих 2:
Для любого объекта POD типа T, независимо от содержимых значений, составляющие объект байты могут быть скопированы в массив типа char или unsigned char. При копировании содержимого массива обратно объект должен вернуться в исходное состояние.
Пример:
#define N sizeof(T)
char buf[N];
T obj; // obj инициализирован некими значениями
std::memcpy(buf, &obj, N); // Между этими двуумя вызовами std::memcpy,
// obj может быть сильно изменен
std::memcpy(&obj, buf, N); // с этого момента obj возвращает свое исходное состояние
Это же в незапамятные времена гарантировал и Си всем своим верным структурам.
Теперь же вопрошает Matt, почему права и свободы гарантированы таким структурам:
struct A {
int n;
};
а вот такие уже лицом не вышли:
struct B {
int n;
B(n_) : n(n_) { }
};
Вот по всем признакам она POD, по всем, кроме наличия определенного пользователем конструктора, который, если бы и хотел, не смог помешать расположению объекта в памяти.
Разработчикам просто выкручивают руки, заставляя их выбирать архаичные типы или полагаться на неопределенное поведение.
Возьмем, к примеру, два массива std::pair<int,int> и побайтово скопируем один в другой. Естественно, мы все полагали бы, что получится два идентичных массива, однако стандарт вам такого не обещал. У std::pair конструктор имеется, а это сразу вычеркивает его из агрегатов, равно как и из привилегированной касты POD. Оправдания, что конструктор там только для красоты, чтоб писать std::pair<int,int> p(1,2); а не странную инициализацию std::pair<int,int> p = {1,2}; отдел цензуры не принимает.
Вот так вот любовь к прекрасному приводила к полной потере гарантий. Хватит это терпеть!
👍6
Beman Dawes против POD
Идею пересмотра незаслуженных привилегий POD типов поддержал и такой маститый разработчик, как Beman Dawes. Активный участник Комитета, много сделавший как для стандартной библиотеки, так и для библиотек Boost, умер в 2020 году. Покойся с миром.
Такой серьезный комитетчик порожняк не гонит, а приводит красноречивый пример из библиотеки Boost:
template <typename T, std::size_t n_bits>
class endian< big, T, n_bits, unaligned > : cover_operators< endian< big, T, n_bits >, T >
{
BOOST_STATIC_ASSERT( (n_bits/8)*8 == n_bits );
public:
typedef T value_type;
endian() {}
endian(T i) { detail::store_big_endian<T, n_bits/8>(bytes, i); }
operator T() const { return detail::load_big_endian<T, n_bits/8>(bytes); }
private:
char bytes[n_bits/8];
};
Капитан говорит, что, судя по коду, это фрагмент библиотеки boos::endian к которой, как мы знаем, приложил руку сам Beman. Приведенный класс формально не является POD, поэтому нет гарантий, что он будет корректно вести себя в разных ситуациях, требующих совместимости по компоновке или гарантий при побайтном копировании объектов. Собственно, а зачем нам еще заботиться о порядке байтов, составляющих объект, если мы не собираемся ничего с ним делать на низком уровне? Нет, манипуляции с порядком байт, сериализация/десериализация объектов (или двоичный ввод/вывод) - это и есть смысл существования таких классов.
Конечно, класс можно вернуть в состояние POD, но для этого придется:
⛔️удалить конструкторы
⛔️сделать все элементы данных общедоступными
⛔️исключить базовый класс
то есть это решительно невозможно, поскольку это означало бы попросту изуродовать интересное решение и деградировать до состояния Си с классами.
Авторы библиотеки и не скрывают, что для стандарта C++03 работа библиотеки основана на неопределенном поведении. Beman с болью констатирует, что такое безрассудство практиковалось даже для промышленных приложений в течение многих лет. Это немного пугает. А мы еще жалуемся на цензоров из approval department, которые нам запрещают таскать в проект всякие сомнительные библиотеки, найденные на помойках интернета.
👍6🤔1
Критика от Lawrence Crowl.
Это, между прочим, тоже не последний разработчик. Он знает толк в параллельном программировании и даже внес существенный вклад в стандарт с++11. Если верить документу WG14 N1196, то долгожданный std::thread и многие другие забавные штуки с ним связанные - его заслуга. Поэтому неудивительно, что в далеком 2006 году он, в рамках обоснования изменений понятия POD, предлагал рассмотреть некий класс, предоставляющий атомарные операции. Такой класс должен быть совместим по компоновке с Си и обязательно быть некопируемым по понятным причинам.
В лучших традициях С++ данные класса должны быть закрыты, но в таком случае класс перестанет быть POD, странно, но это неизбежно вытекает из правил, существовавших до 2006 года.
Стоило бы разрешить именовать гордым именем POD также и структуры, все члены которых имеют одинаковый уровень доступа. Действительно, что плохого может случится, если все члены будут закрытыми? Это же не помешает копировать объект побайтово.
С запретом копирования все сложнее, даже раскрепощение определения не позволит делать объекты некопируемыми.
Думаю, многим приведенное выше описание покажется подозрительно похожим на std::mutex, который соответствует определению Standard Layout Type, а за попытку копировать или переместить мьютекс будут бить батогами.
Впрочем, это дела давно минувшего будущего, а тогда, в 2006, очевидно было одно: масонский заговор против POD созрел.
👍9
Опасный optional для IAR
Наш старый приятель, близкий друг подруги девушки брата когда-то считал, есть у IAR существенный изъян: std::optional отсутствует как класс! Не фатальный, конечно, недостаток, но мешает чувствовать себя стильным и модным. Уж не знаю, чем не угодила эта концепция особенным разработчикам стандартной библиотеки IAR, может быть, они считают, что код должен быть четким без сомнений и колебаний. Так вот, он решил тогда молча не страдать, а собраться и написать свой optional. И если путь в тысячу ли начинается с первого шага, то путь, полный боли и отчаяния, начинается с мысли: "Подумаешь, там же ничего сложного!".
В соответствии с принципом KISS, измученный наш друг слепил из подручных материалов такое вот поделие:
template <class T>
class Optional {
public:
constexpr Optional() = default;
constexpr Optional(T const &v) : t_{v}, is_exist_{true} {}
constexpr Optional& operator=(T const v) {
t_ = v;
is_exist_ = true;
return *this;
}
...
private:
T t_ {};
bool is_exist_{false};
};
Обычные варианты использования приводят ко вполне ожидаемому результату:
Optional<int> opt1 {};    // t_ == 0, is_exist_ == false
opt1 = 2; // t_ == 2, is_exist_ == true
Optional<int> opt {1}; // t_ == 1, is_exist_ == true
Эта наивная реализация работает как и стандартный класс, а может и лучше. Причем настолько, что один из пользователей класса от избытка чувств пообещал задушить автора в объятиях и трижды переехать машиной. Его можно понять, ведь после выражения
opt = {}; 
мы все ожидали бы получить пустой объект типа Optional, интуитивно считая, что это
opt = Optional<int>{}. Однако компилятор считает иначе. Для него это выражение opt = int{}. То есть мы получим 0 как значение, и is_exist_ будет равен true. Это противоестественно, что спешит подтвердить GCC реализация такого класса: после такого присваивания объект гарантированно будет пуст.
Виной всему, как вы уже догадались, оператор присваивания Optional& operator=(T const v). Он перехватит присваивание пустых скобок у копирующего оператора присваивания и попутно оставляет не у дел конструктор по умолчанию. Надавать по рукам этому оператору можно, сделав его чуть более шаблонным для пущей строгости:
template <class U, std::enable_if_t<std::is_same<T, U>::value, bool> = true>
constexpr Optional& operator=(U const v) {...}
Тем самым "выключаем" его для неподходящих типов, нужно точное соответствие типу int без сомнительных приведений.
Либо можно просто убрать оператор присваивания из определения класса. Путь внедрения значение в Optional должен быть один - через конструктор, что соблюдается в стандартной библиотеке, кстати.
Вот так, обремененный этой сермяжной правдой и добрыми пожеланиями от коллег, наш герой сел пить чай с конфетами.
👍8
Media is too big
VIEW IN TELEGRAM
Using C++14 in an Embedded “SuperLoop” Firmware
Erik Rainey
CppCon 2022 наконец-то порадовал докладами для базовой подготовки embedded разработчика. Здесь его научат выживать в тяжелых условиях отсутствия какой-либо операционной системы, как того требует парадигма SuperLoop. Спокойно, Эрик расскажет, что это и с чем едят, и пояснит, почему некоторые возможности языка и стандарта C++14 используются, а некоторые нет.
В общем-то, это похоже на сборник хороших практик, которые не стыдно будет применять в боевом проекте, чтоб убеленные сединами сеньоры благосклонно кивали, глядя на ваш код.
👍9
Embedded_Cpp14_Superloop_Firmware_Public.pdf
943.4 KB
В материалы конференции слайдов не положили, утащил из другого места. Почти совпадает.
👍9
std::unique_ptr
Ходят слухи, что умные указатели не применимы в embedded. Особенно в мелкопоместных встраиваемых системах, с которых, кроме микроконтроллера, и взять-то нечего. Возможно, в парадигме SuperLoop и вправду можно обойтись без этого, но если вдруг в проекте завелась маленькая операционная система хотя бы с жалким подобием потоков, то мгновенно прибегает хор коллег с песнями о пользе умных указателей. Главное, вовремя остановить их, пока миру еще не явлен новый unique_ptr. Зачем? Так стандартный же стоит в разделе "Dynamic memory management", что намекает на близкую связь с динамическим выделением памяти, бежать от которой следует быстрее гепарда. Так-то оно так, только немного не так: сам умный указатель не распределяет память, не вызывает ни new, ни delete. Он выше всего этого.
Обычно unique_ptr содержит в себе что-то вроде std::tuple<pointer, _Dp>
где pointer, очевидно, указатель на объект, которым предстоит уникально владеть
_Dp это тип уничтожителя объекта.
Концепция проста до леденящего душу ужаса: сообразительный указатель сохраняет указатель на объект, а также инструкцию как этот объект уничтожить.
Динамическим же выделением памяти занимается std::make_unique, она, проказница, и вызывает оператор new. Можем попробовать обойтись и без него:
int y = 0;
int *ptr = &y;
auto x = std::unique_ptr<int>(ptr);
Работает! Только уничтожение объекта x приведет к печальным последствиям из-за уничтожителя по умолчанию! Вот он в описании класса:
template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr { ... };
Если не указано обратное, то уничтожитель - это default_delete (Кстати, отсюда видно, что умный указатель не работает с указателями void*):
template<typename _Tp>
struct default_delete {
constexpr default_delete() noexcept = default;
void operator()(_Tp* __ptr) const {
static_assert(!is_void<_Tp>::value, "can't delete pointer to incomplete type");
static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type");
delete __ptr;
}
};
Этот тихий убийца попытается укокошить объект через вызов delete для указателя. Поэтому нам лучше от него отказаться и определить свой тип unique_ptr.
template<typename _Tp>
struct custom_delete {
constexpr custom_delete() noexcept = default;
void operator()(_Tp*) const {}
};
void do_nothing(int *) {}
namespace my {
using unique_ptr1 = std::unique_ptr<int, void(*)(int *)>;
using unique_ptr2 = std::unique_ptr<int, custom_delete<int>>;
using unique_ptr3 = std::unique_ptr<int, decltype(&do_nothing)>;
} // namespace my
...
auto x = my::unique_ptr1(ptr, +[](int *){});
auto x1 = my::unique_ptr2(ptr);
Во втором случае конструктор не потребует объект уничтожителя явно, он будет создан на месте.
👍6🔥1
Разложение POD
2007 год принес нам новое понимание POD типов. Чтоб разрешить накопившиеся противоречия и умиротворить разбушевавшихся ботанов, комитет принимает историческое решение рассматривать POD как частный случай суперпозиции двух свойств: тривиального класса (trivial class) и класса со стандартным выравниванием (standard-layout class).
Предлагалось добавить определение тривиального класса, который обладает следующими признаками:
🔹 тривиальный конструктор по умолчанию;
🔹 тривиальный копирующий конструктор;
🔹 тривиальный копирующий оператор присваивания;
🔹 тривиальный деструктор.
Напомню ради смеха, что тривиальный конструктор по умолчанию, как и хороший сисадмин, не делает ничего! С работой остальных копирующих конструкторов могла бы справиться и функция memcpy. Чтоб было еще веселее, заметим, что такое определение исключает виртуальные функции и виртуальные базовые классы.
Собственно, понятие тривиальности и введено, дабы гарантии побайтного копирования распространялись и на не-POD классы. Да, теперь их можно невозбранно сериализовать, переносить и восстанавливать из байтов простейшими функциями работы с памятью.
Тип стандартной компоновки в добавленном определении означал, что класс:
🔸 состоит (или наследуется) из себе подобных;
🔸 не содержит виртуальных функций и виртуальных базовых классов;
🔸 содержит члены-данные одинакового уровня доступа;
🔸 при наследовании все данные должны содержаться только в одном классе;
🔸 не содержит членов такого же типа, что присутствует в иерархии.
В общем, все то, что делает класс предсказуемым в плане размещения его в памяти. Еще указатель на такой класс может быть приведен к указателю на его первый член или наоборот. Идеально подходит, если надо обмениваться структурированными данными с менее прогрессивными языками.
Немного примеров для преодоления когнитивного диссонанса:
struct N { // нетривиальный класс и не класс со стандартным выравниваем
int i;
int j;
virtual ~N();
};
struct T { // тривиальный, но не класс со стандартным выравниваем
int i;
private:
int j;
};
struct SL { // нетривиальный, но класс со стандартным выравниваем
int i;
int j;
~SL();
};
struct POD { // тривиальный класс со стандартным выравниваем
int i;
int j;
};

Ну вот и все, теперь POD тип больше не феноменальный агрегат, а класс, который просто соответствует двум понятиям из языка С++. Комитет так ловко разложил POD на составляющие, что удачное определение продержится еще очень долго, почти до самого упразднения рудиментарного понятия.
👍10
IAR & unique_ptr
Знаете, что значит IAR? In a relationship. У кого IAR, мои конгратуляции.
Вот и мой новый проект внезапно отказался собираться. При GCC такого не было, а любимый компилятор опять исподтишка нанес подлый удар. С чувством брезгливости достаю обгаженный IAR-ом вывод консоли и принюхиваюсь, пытаясь по запахам кода определить источник проблем.
Компилятор говорит, что собака зарыта здесь:
  : _Myval1(), _Myval2(std::forward<_Other2>(_Val2)...)
Это сильно похоже на внутренности стандартной библиотеки, круг подозреваемых резко сужается.
"arm/inc/cpp/xutility",408  Error[Pe808]: 
default-initialization of reference is not allowed
detected during:
Ага, понятно, где-то провалилась наивная попытка инициализировать какой-то объект по умолчанию. Глупая ошибка, но где?
Скользим взглядом вниз, оставляя без внимания малозначительные жалобы компилятора на неудачные инстанцирования.
            instantiation of "inline std::unique_ptr<_Ty, _Dx>::unique_ptr(std::unique_ptr<_Ty, _Dx>::pointer)
[with _Ty=int, _Dx=Deleter &]"
И ты, unique_ptr! Вот уж от кого не ожидал, ведь в роли чистильщика выступает ссылка на функтор, а уж если один из членов класса имеет ссылочный тип, то инициализация по умолчанию не применима в принципе. Проблема в том, что в моем коде есть защита от такого типа недоразумений: проверка std::is_default_constructible. Условно говоря, мы и пытаться не будем создавать значение по умолчанию для unique_ptr благодаря SFINAE. В GCC так и происходит, а IAR дает сбой. Неужели снова?..
static_assert(std::is_default_constructible<std::unique_ptr<int, Deleter &>>::value)  
// GCC - false, IAR - true
Да, IAR снова врет нам в глаза, будто студент, курсовую работу которого съела собака.
Наученный горьким опытом, лезу сразу в реализацию стандартных умных указателей.
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr : public _Unique_ptr_base<_Ty, _Dx> {
...
constexpr unique_ptr() _NOEXCEPT : _Mybase(pointer()) {
// default construct
Так и есть, конструктор по умолчанию хвастливо заявляет, мол, из любых типов смогу создать unique_ptr, во что и спешит верить наивная метафункция std::is_default_constructible.
Могли ли разработчики предотвратить досадное недопонимание? Однозначно да, только взгляните на реализацию умного указателя в GCC:
template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr {
...
template <typename _Up = _Dp, typename = _DeleterConstraint<_Up>>
constexpr unique_ptr() noexcept : _M_t() { }

Какое изящество! Сделать конструктор по умолчанию намеренно шаблонным, чтоб с помощью SFINAE отключить его для умных указателей, уничтожитель которых такого конструктора не имеет. (_DeleterConstraint проверит возможность инициализации по умолчанию типа уничтожителя).
Практически IAR накладывает ограничение на тип уничтожителя: он должен иметь возможность быть созданным по умолчанию, тогда все несостыковки рассасываются сами собой. В общем, переходите дорогу на зеленый сигнал светофора, а шаблоны используйте осмотрительно.
👍9
Static Initialization Order Fiasco
Незаслуженно забытым оказалось такое явление, как "фиаско порядка статической инициализации". Для embedded проектов может стать большой проблемой, если вы любите действовать на опережение и конструировать объекты до вызова главной функции main. Например, велико искушение представить слой BSP или driver как большой статический объект. Никто не запрещает, конечно, но делать это нужно надлежащим образом.
Вот довольно безграмотный пример для демонстрации проблемы
struct A {
A() : a_{42} {}
int a_;
};
struct B {
B() : b_{sa_.a_} {}
int b_;
static A sa_;
} b {};

A B::sa_{};
если запросить в функции main значение b.b_, то оно с большей вероятностью будет равно 0, а не 42, как мы надеемся. Все дело в том, что когда инициализируется переменная b_, статический объект sa_ еще не готов, под него выделена память, но вся она заполнена в лучшем случае нулями. Все может закончиться не так безобидно, если в конструкторе статического объекты мы пытаемся вызвать виртуальный метод другого, еще не инициализированного объекта. Дело тут будет пахнуть уже разыменованием нулевого указателя.
struct Config final {
Config() : x_{obj_->Calc()} {}
int x_;
static Base *obj_;
} conf {};
static Object glob {};
Base *Config::obj_{&glob};
Хоть obj_ будет точно указывать на объект glob, но vtable ему еще не завезли, и это фиаско, братан! Когда все это содержится в одно единице трансляции, неправильный порядок еще можно легко обнаружить, а вот если это все размазано по нескольким, то придется поломать голову.
Еще одна опасность - потеря всех данных. Вы можете кропотливо заполнять статический std::array данными при создании другого статического объекта, но потом будет вызван конструктор стандартного массива, что заботливо заполнит память нулями. Все ваши данные пропали вместе с хорошим настроением. Повезет еще, если это закончится просто нестабильной работой системы, а не психики.
Защититься от беды можно с помощью хорошо известной идиомы Construct On First Use, "создай объект при первом использовании". Проще говоря, функциональное обертывание статического объекта гарантирует его инициализацию при первом получении доступа к нему.
То есть для первого примера достаточно изменить класс B:
struct B {
B() : b_{GetSA().a_} {}
int b_;
static A &GetSA() {
static A sa_{};
return sa_;
}
} b {};
Да, это почти синглтон, тут используется тот же принцип. Это хороший метод, надежный, защитит даже от коварства IAR, о котором расскажу в следующий раз.
👍8
Коварство IAR
Обычно вероломство компиляторов проявляется в необычных условиях: где-то в районе пересечения сомнительных практик и нарушения стандартов, когда код уже начинает немножечко источать невыносимое амбре.
Статические объекты - это тот еще источник головной боли, хотя их и готовы терпеть в приличном обществе, где бы за глобальные переменные давно подняли на вилы, но разница не такая уж и большая. Как говорил AUTOSAR, неконстантные глобальные и статические переменные, как и наркоманы, скрывают настоящие зависимости, поэтому код становится невозможно читать, тестировать и поддерживать. Ибо поддерживать можно только то, что стоит, а не лежит лапшой в репозитории.
Рекомендации AUTOSAR оставим на потом. Лучше рассмотрим простой пример статического объекта:
struct Base { 
virtual ~Base() = default;
};
struct Point final : public Base {
Point() = default;
~Point() final = default;
int32_t x{1};
int32_t y{2};
int32_t z{3};
};
static Point t{};

Здесь и думать нечего, начальное состояние объекта можно просчитать заранее: указатель на vtable, три переменных, никаких определенных пользователем конструкторов. Для инициализации объекта достаточно скопировать слепок памяти из ROM в RAM, то есть все нужные значения появятся при начальной загрузке памяти (одно из первых действий, выполняемых после старта устройства).
Собственно, IAR так и накомпилирует:
t:
DATA32
DC32 vtable for Point + 0x8, 1, 2, 3

Довольно невинно выглядит, пока мы не внесем сумятицу в код:
struct Point final : public Base {
...
int32_t y{2};
int32_t z;
};

Все то же самое, только для члена класса z мы "забыли" написать значение по умолчанию. Да, это не очень здорово, бросать переменные на произвол судьбы, но никакого криминала мы не ожидаем. Собираем и....
__sti__routine:
PUSH {R3-R5,LR}
MOVS R1,#+16
MOVS R2,#+0
LDR.N R4,??DataTable1
MOVS R5,R4
MOVS R0,R5
BL __aeabi_memset
MOVS R0,R4
BL Point::Point() [complete object constructor]
POP {R0,R4,R5,PC} ;; return

Ого! Во-первых, теперь для инициализации нашего статического объекта IAR отрядил целую функцию __sti__routine, как если бы у нас был объект со сложным конструктором. Во-вторых, он создал для Point настоящий конструктор, хотя он помечен как default. Видимо, если IAR видит неинициализированный член класса, он ожидает, что начальное значение будет положено туда в конструкторе. Собственно в функции инициализации нетривиальных статических объектов __sti__routine, мы видим, что __aeabi_memset кучно кладет нули в первые 16 байт по адресу статического объекта t, а потом на этом месте вызывается конструктор. Много появилось лишних телодвижений, но с этим можно жить. Так ведь?
Как говорится, код познается в релизной сборке.
t:
DATA32
DC32 vtable for Point + 0x8, 1, 2
DS8 4
...
__sti__routine:
LDR.N R0,??DataTable1
MOVS R1,#+0
MOVS R2,#+0
MOVS R3,#+0
MOV R12,R1
STM R0,{R1-R3,R12}
BX LR ;; return

Высокий уровень оптимизации приводит к тому, что статический объект получает все известные значения при инициализации памяти, все, кроме z. Брошенная переменная провоцирует создание функции __sti__routine, где IAR пытается провернуть тот же трюк, что и прошлый раз: зачистить память и вызвать конструктор. Как видите, получилась только первая часть. Конструктор оптимизирован, его нет, вызывать больше нечего. В main объект войдет абсолютно очищенным, гордо демонстрируя нули везде, даже в vtable. Как вы уже догадались, при попытке вызвать
любую виртуальную функцию, мы получим вполне реальное падение системы.
Конечно, можно сказать, что это баг компилятора, но и код ведь не совсем корректен, правила AUTOSAR тут грубо попраны с немыслимой жестокостью, чего же теперь выть и сокрушаться?
👍9
AUTOSAR и непознанные статические объекты
Правило A3-3-2
Статические объекты и локальные объекты потока (thread-local) должны быть инициализированы константно.
Сформулировано не очень понятно, но, как мы уже поняли, соблюдение правил может уберечь от подозрительного поведения строптивых компиляторов в странных условиях. Поэтому это правило лучше соблюдать не задумываясь. Хотя, если задуматься, смысл тут тоже присутствует.
Упомянутая константная инициализация означает, что статический объект при создании заполняется исключительно константами времени компиляции. Отсюда логично воспоследует, что такой объект не требует дополнительных манипуляций при создании. У него есть бинарный образ, который просто накладывается на объект. Если в статическом объекте вам нужна сложная инициализация, то что-то с вами не так.
Компилятор исполнит это сакральное таинство только в случаях инициализации значением, константным выражением или при вызове constexpr конструктора (только с константными выражениями в качестве аргументов). То есть все то, что можно посчитать на этапе компиляции и впихнуть в ROM память, куда-нибудь в секцию .data или .rodata. Чтоб потом, при старте, сразу после срабатывания обработчика сброса (Reset_Handler) скопировать эти данные по нужному адресу. Собственно и все, значения окажутся на своих местах, объект готов к труду и обороне.
Поэтому и говорят, что главное достоинство инициализации подобного вида в том, что она отработает раньше всех без шума и пыли, безо всяких коллизий и фиаско.
Возьмем какой-нибудь уже хорошо нам знакомый класс Point.
struct Point {
Point() = default;
int x {1};
int y {2};
};
static T center {};
Компилятор легко создаст образ этого объекта во время сборки, поэтому никаких вызовов конструкторов мы не увидим:
center:
.word 1
.word 2
Если же мы используем собственноручно написанный конструктор
struct Point {
Point() : x{1}, y{2} {}
int x;
int y;
};
то внезапно все действия по инициализации мы узрим в отдельной функции. GCC создаст __static_initialization_and_destruction_0(int, int), а в IAR грязной работой занимается __sti__routine. Это все потому, что конструктор хоть и простой как апельсин, но не constexpr.
Пометим его правильно
constexpr Point() : x{1}, y{2} {}
и компилятор теперь с облегчением выкидывает функцию __static_initialization_and_destruction_0 и возвращается к первому варианту распределения значений в памяти.
Я так мыслю, это и есть главная цель правила: избежать создания дополнительных функций для инициализации глобальных объектов, ибо это уже не тривиальное действие, а граф Толстой завещал опрощаться.
👍8🤔1
Идиома C++: Execute-Around Pointer
Думаю, у всех бывали ситуации, когда кровь из носу нужно выполнять определенные действия перед вызовом метода класса или сразу после. Желательно не модифицируя сам метод. Например, защитить мьютексом: заблокировать его перед вызовом и отпустить с миром после. Классический вариант предусматривает использование RAII из стандартной библиотеки:
struct A {
std::mutex mtx_ {};
void SafeSmth() {
std::lock_guard<std::mutex> lock{mtx_};
...
}
};
Приемлемо, но что делать, если лень копировать такую защиту во все функции класса? Надо мыслить ширше. Во-первых, нужно подготовить класс-обертку, который будет содержать в себе защищаемый объект или хотя бы чахлую ссылку на него. Во-вторых, необходим некий временный прокси-объект, на который и будет возложена почетная миссия. В-третьих, насадим это все хозяйство на operator->:
template <class T>
struct guarded_object {
guarded_object(T &t) : t_{t} {}

struct proxy_object {
proxy_object(T &t, std::mutex &m) : t_{t} {}
~proxy_object() {}
T *operator->() {
return &t_;
}
T &t_;
};

proxy_object operator->() {
return proxy_object{t_, mtx_};
}
T &t_;
std::mutex mtx_{};
};
Внутренний прокси-объект при создании блокирует мьютекс, при уничтожении отпускает. Но между этими событиями вызывается operator->, который предоставляет доступ к защищенному объекту, и мы можем вызвать любую его функцию. Использовать это следует так:
Object obj{};
guarded_object<Object> safe_obj {t};
safe_obj->DoSmth();
Может показаться странным, что используется только один operator->, но все верно. В данном случае он будет эквивалентен вызову
safe_obj.operator->().operator->()->DoSmth();
Удобно, это маскирует малозначительные детали от пользователя. В то же время сложнее подобраться к целевому объекту, однако помните, что это возможно:
auto x = safe_obj.operator->().operator->();
x->DoSmth(10);
Здесь вызов DoSmth будет вызван без прикрытия мьютекса, поскольку мы получили прямой доступ к объекту. Это недостаток идиомы. Хорошо, что неправильно использовать этот подход неудобно. Как воскликнул мой коллега, когда я ему рассказал об этой лазейке: "Но каким же извращенным сознанием нужно обладать, чтоб сделать ТАК?". Я многозначительно промолчал.
С праздником Победы всех, кто дочитал!
👍9
AUTOSAR
Правило A12-0-1
Если в классе объявлены через "=default" и "=delete", либо через пользовательские определения, операции копирования или перемещения, или же деструктор, тогда вся пятерка специальных функций-членов должна быть объявлена.
Архиважное правило! Помню тот день, когда мы, решив сделать наш код безопасным, как одноразовая бритва, в одно мгновение перенеслись в волшебный мир AUTOSAR. В той сказочной стране у всех разработчиков красивые квадратные глаза от обилия найденных несоответствий стандарту. Самое распространенное нарушение было связано с "правилом пяти". Ведь "Правило нуля" подходит для каких-нибудь структур, где указывать деструктор и иные методы необязательно, да и запрещено. Во всех остальных случаях без них не обойтись, согласно уже другим правилам AUTOSAR. Так и выходит, что почти в каждом классе должны быть определены:
🔹 деструктор,
🔹 копирующий конструктор,
🔹 перемещающий конструктор,
🔹 копирующий оператор присваивания,
🔹 перемещающий оператор присваивания.
В итоге приемлемое объявление класса выглядит примерно так:
class A {
public:
...
~A() = default;
A(const A &) = default;
A(A &&) = default;
A &operator=(const A &) & = default;
A &operator=(A &&) & = default;
...
};
Намеренно не включаю в пример конструктор, его в "клуб пяти" никто не звал. Даже конструктор по умолчанию. Все равно он исчезнет, как только появится пользовательское определение.
Хотя никто не мешает явно обозначить присутствие дефолтного конструктора, если без него ну никак не обойтись, как шаману без третьей руки. Однако некоторые, с позволения сказать, коллеги агрессивно настаивают, что нужно обязательно явно удалять конструктор по умолчанию. Мне же кажется это избыточным и бесполезным занятием, от которого на ладошках заколосится густой волосяной покров. К сожалению, митинги-поединки на ржавых цепях запрещены, поэтому попробуем показать, как это сбивает с толку даже лучшие из компиляторов. Добавим к нашему определению следующий конструктор:
A(int t) : t_{t} {}
Тогда наш класс выбывает из разряда тривиальных из-за пользовательского конструктора. Впрочем, если добавить
A() = default;
то все вернется на круги своя, явный конструктор по умолчанию делает класс опять тривиальным.
Хорошо, но теперь мы решили показать и ежам, что никакого конструктора по умолчанию не было, нет и не будет!
A() = delete;
По смыслу ничего не изменилось, конструктора как и не было, так и нет, однако внезапно мы проходим следующую проверку:
static_assert(std::is_trivial_v<A>);  // OK
Все едино: что явно добавили, что удалили. Класс тривиальный и все тут. GCC так видит, хотя это уже противоречит определению. Вот в IAR не так, там после удаления конструктора класс навсегда теряет свою тривиальность, и никакие приседания ее не вернут. Это похвально, но анализатор AUTOSAR тоже немного сбит с толку бесполезной строчкой кода и иногда выдает странные ошибки в тех местах. Продолжаю наблюдение.
👍6🤔2
std::atomic
"Да зачем нам этот вам пижонский atomic, у нас есть исконно-посконный volatile!" - скажут некоторые. Такие люди восхищают, они не боятся трудностей, гвозди бы делать из таких. Большое человеческое аригато, благодаря им, в с++20 некоторые варианты использования этого ключевого слова признаны нежелательными. Вообще, volatile не подразумевает атомарного доступа и не занимается упорядочиванием обращений к памяти. Для этих целей придумали именно std::atomic, через который можно определить атомарный тип (std::atomic<int> или даже std::atomic<bool>, например). Если объект атомарного типа используется в нескольких потоках, то почему бы и не упорядочить к нему доступ? Некоторые радикалы утверждают, что можно обезопасить объекты, которые нещадно эксплуатируются одновременно и в прерываниях, и в потоках (либо в единственном SuperLoop), но здесь мы ступаем на зыбкую почву, и делать это стоит с осторожностью, будто ранней весной гуляешь по газону.
Надо сказать, реализован атомарный шаблон довольно скучно. Почти сразу упирается в некие встроенные функции, что мешает нам постигнуть сермяжную правду о безопасности std::atomic.
Что же, когда нас это останавливало? Возьмем простейшие инструкции store/load и заглянем в ассемблерный выхлоп GCC (для архитектуры armv7).
std::atomic<int> a {};
int x {};
a.store(x, std::memory_order::memory_order_relaxed);
x= a.load(std::memory_order::memory_order_relaxed);
Кстати, модель памяти тут можно и не указывать, результат покажет удивительное постоянство.
Если убрать всю шелуху, то вот основные инструкции для load:
dmb ish
ldr r3, [r3]
dmb ish
для store:
dmb ish
str r2, [r3]
dmb ish
В лучших традициях действие помещают между инструкций Data Memory Barrier, которые упорядочат все явные обращения к памяти. Задорная опция ish значит только, что наведение порядка нужно для внутреннего общего домена памяти.
Если же рискнуть и легким движением руки изменить архитектуру -march=armv8-m.main, то произойдет чудо, и наша пара ldr/str будет заменена на lda/stl, барьеры памяти исчезнут, т.к. больше не нужны. На самом же деле ничего сверхъестественного не произошло, все согласуется с рекомендациями c/c++11 mapping to processors.
Удивительно, но IAR не будет выделяться оригинальностью:
std::atomic<int, void>::store(int, std::memory_order):
...
DMB SY
STR R1,[R0, #+0]
DMB SY
BX LR ;; return
std::atomic<int, void>::load(std::memory_order) const:
...
DMB SY
LDR R0,[R2, #+0]
DMB SY
BX LR ;; return
Хорошо знакомые нам инструкции, только барьер памяти с опцией SY применим для всей системы. Хотя разницу заметить очень сложно. Для иной архитектуры:
<std::atomic<int, void>::load(std::memory_order) const>:
...
lda r0, [r2]
bx lr
<std::atomic<int, void>::store(int, std::memory_order)>:
...
stl r1, [r0]
bx lr
Уже хорошо знакомые нам метаморфозы.
Можно ли использовать их в прерываниях? Скорее да, такие атомарные типы реализованы как неблокирующие или lock free. Почему бы и нет, если архитектура дает зуб, что операции чтения/записи будут неделимыми, ведь размер объекта меньше или равен 32 битам, ну еще выравнивание не должно быть противоестественным, но это уже детали.
Как ведет себя гигантский объект атомарного типа и опасен ли он для окружающих, мы скоро узнаем. A suivre.
👍9🤔1
King-size std::atomic
Если ваш атомарный тип перестал влезать в регистр, то стоит насторожиться. Избыточный объем объекта приводит к некоторым сложностям: доступ к телу одной операцией чтения или записи не удовлетворится, а гарантировать неделимость целой серии манипуляций с памятью будет куда сложнее пареной репы. С грустью машем ручкой легковесными механизмами lock-free и вступаем в неизведанные земли аппаратной зависимости. Для разных архитектур нетривиальные задачи атомизации могут решаться разными путями, например, для -march=armv7-a+simd
метод store будет выглядеть так:
.L15:
ldrexd r4, r5, [r1]
strexd r0, r2, r3, [r1]
cmp r0, #0
bne .L15
dmb ish
Стало быть, у процессора есть специальный узел - монитор адресов (address monitor), следящий за обращениями к памяти. Пара инструкций ldrexd/strexd делит операцию атомарного обновления памяти на два этапа:
LDREXD загружает двойное слово из памяти по адресу в регистре r1 в регистры r4 и r5, переводит монитор в состояние эксклюзивного доступа.
STREXD пытается изо всех сил сохранить слова r2 и r3 в память. Если монитор находится в состоянии эксклюзивного доступа, то операция обновляет ячейку памяти и возвращает значение 0 в регистр r0 как признак успешного успеха. Если эксклюзивный доступ аннулирован, то операция не обновляет ячейку памяти и возвращает значение 1 в регистре r0. Эксклюзивный доступ может быть отменен, если по какой-то причине между этими двумя инструкциями происходит несанкционированный доступ к бережно опекаемой ячейке памяти.
У архитектур -march=armv7-m, -march=armv8-m.main все решается проще, вызовом экспортируемых функций __atomic_store_8 и __atomic_load_8.
Если же размер объекта будет больше, например, 16 байт:
struct Huge { int64_t x,y; };
то компилятор позовет __atomic_store и __atomic_load.
Чем хорош GCC, он без обиняков скажет, что не знает как сделать для таких гигантов атомарный доступ. Вежливо намекнет, что дальше вы сами:
undefined reference to `__atomic_load_8'
IAR же уверен, что лучше знает как поступать в подобных ситуациях, чем малограмотный разработчик. Примерная реализация функции загрузки в исполнении лучшего компилятора выглядит так:
void __atomic_load(int n, void *asrc, void *dest, int mo) {
...
lock_handle hL = lock(asrc);
memcpy(dest, asrc, n);
release(hL);
}
Блокируем доступ к телу, неспешно копируем, возвращаем доступ. Реализация блокирующей функции:
static atomic_flag locks[4] = {ATOMIC_FLAG_INIT, ...};
static lock_handle lock(atomic_ptr ap) {
uintptr_t idx = (uintptr_t)ap;
idx = (idx & 0xC) >> 2;
atomic_flag * lock_ptr = &(locks[idx]);
while(atomic_flag_test_and_set(lock_ptr));
return lock_ptr;
}
Суть в том, что есть четыре байта locks, где хранятся глобальные состояния, или флаги для всех крупных атомарных объектов. Какой именно флаг будет использован, вычисляется по передаваемому адресу ap (адрес атомарного объекта). Дальше в дело вступает техника спин-блокировки (spinlock), хорошо знакомая еще по ядру Linux. Мы просто в цикле будем ждать, пока не сможем заменить глобальное состояние с нуля (свободно) на единицу (занято).
Теоретически может получится так, что после блокировки, например, locks[0], произойдет прерывание, внутри которого некий атомарный объект тоже будет использовать флаг locks[0]. Попытка его изменить или прочитать приведет к повторной блокировке состояния, и мы прочно засядем в бесконечном цикле spinlock, как Ever Given на мели в Суэцком канале.
Впрочем, поймать такой deadlock в боевой обстановке не очень-то и получилось, слишком много звезд должно сойтись. Что значит, известная C++ идиома "авось" превосходно работает и тут.
👍6🤯2🤔1