Как стать автором
Обновить

Разработка firmware на С++ словно игра в бисер. Как перестать динамически выделять память и начать жить

Время на прочтение18 мин
Количество просмотров12K
Всего голосов 52: ↑50 и ↓2+48
Комментарии87

Комментарии 87

Да, необходимость отказываться от динамических аллокаций довольно сильно мешает писать прошивки на С++; но на мой взгляд это все равно лучше, чем писать на С — да, стандартными контейнерами не попользуешься (но можно написать свои или взять https://www.etlcpp.com/), да, std::function не поюзаешь (но можно написать свою реализацию без динамических аллокаций).
К счастью, все эти вещи можно отключить буквально тремя опциями компилятора, и форсировать это на уровне соглашения о стиле кода.


Зато шаблоны, лямбды, RAII и ООП без ручных манипуляций с указателями.

Все верно. Особенно от ООП (а именно от инкапсуляции и конструкторов/деструкторов) пищать хочется. Но если отключаешь динамику, то начинается адище — либо как в статье, либо ETL (который имхо в обозримом будущем подавится и упадет под диван), в общем широкий набор способов создать новый язык. Это плохо само по себе, к тому же туго ложится на индустрию.

Кстати, почему тремя?
Но если отключаешь динамику, то начинается адище — либо как в статье, либо ETL (который имхо в обозримом будущем подавится и упадет под диван), в общем широкий набор способов создать новый язык. Это плохо само по себе, к тому же туго ложится на индустрию.

Я как-то без контейнеров стандартных и исключений вполне себе обхожусь, на самом деле; больше не хватает асинхронщины какой-нибудь, но она на плюсах пока что выглядит довольно мерзко в любом случае.


(Правда велосипеды вместо std::function и std::span таки изобрелись)


Кстати, почему тремя?

Ну, обычно что-то в духе -fno-exceptions -fno-rtti и еще какой-нибудь финт, чтобы динамику отключить. В опциях gcc я не так хорошо ориентируюсь, вполне может быть достаточно сделать --specs=nano.specsвместо трех отдельных опций.

больше не хватает асинхронщины какой-нибудь, но она на плюсах пока что выглядит довольно мерзко в любом случае


Ммм, вы находите? Вроде норм… Но вообще, как по мне — уж лучше в потоки, я к асинхронщине в принципе отношусь с сомнением.

Я как-то без контейнеров стандартных и исключений вполне себе обхожусь


Да, но не использовать контейнеры — это же почти идиоматическое преступление, могут и к высшей мере приговорить в интернетах!!!111

Впрочем это может быть интересной концепцией — запретить вообще пространство имен STL. Только сишные либы (выпилив и из них динамику) и сам стандарт языка. То есть все эти вот извращения вокруг STL это примерно как мамкино веганство («курицы глупые, их можно есть»), а вот такой подход — чистое, настоящее веганство.

Правда велосипеды вместо std::function и std::span таки изобрелись


М, да, кстати, спан бывает полезен, впрочем я и к нему с сомнением отношусь.

Ну, обычно что-то в духе -fno-exceptions -fno-rtti и еще какой-нибудь финт, чтобы динамику отключить


А, ну вы про РТТИ еще вспомнили, точно. Главное же кучу задать нулевой!
Ммм, вы находите? Вроде норм… Но вообще, как по мне — уж лучше в потоки, я к асинхронщине в принципе отношусь с сомнением.

Можно и потоки, лишь бы не городить полотнища свитчей для конечных автоматов. С другой стороны, зато все просто и понятно :)


Да, но не использовать контейнеры — это же почти идиоматическое преступление, могут и к высшей мере приговорить в интернетах!!!111

В embedded — скорее наоборот, насколько я могу судить; многие придерживаются мнения, что С++ в принципе — это недопустимо.


Впрочем это может быть интересной концепцией — запретить вообще пространство имен STL. Только сишные либы (выпилив и из них динамику) и сам стандарт языка. То есть все эти вот извращения вокруг STL это примерно как мамкино веганство («курицы глупые, их можно есть»), а вот такой подход — чистое, настоящее веганство.

Но в std есть куча полезных вещей, которые с контейнерами не связаны — скажем, std:nth_element или std::atomic; контейнеры в отдельный неймспейс не выделили.


Можно, конечно, просто несколько контейнерных хедеров "запретить", но зачем — если запретить динамическую аллокацию, то с ними все равно линковаться не будет.


А, ну вы про РТТИ еще вспомнили, точно. Главное же кучу задать нулевой!

Просто именно куча опцией-то как раз не выключается, по крайней мере в Кейле для этого прагму надо писать, в gcc или ставить нулевой размер кучи или какой-то костыль делать типа --wrap=malloc. Мб в IAR опция, не знаю.


М, да, кстати, спан бывает полезен, впрочем я и к нему с сомнением отношусь.

Меня лично спан (как и все остальные контейнеры STL) огорчает в основном тем, что он не проверяет выход за границы в операторе [] — а легко везде заменить [] на at — не особо получается.


Но спан все равно лучше, чем отдельно указатель и размер.

Можно и потоки, лишь бы не городить полотнища свитчей для конечных автоматов


А вы про какую ситуацию? Просто какой-нибудь парсер со сложным автоматом на кучу потоков не заменишь. Или вы про другое?

В embedded — скорее наоборот, насколько я могу судить; многие придерживаются мнения, что С++ в принципе — это недопустимо.


Да, и оно ведь во многом растет от библиотеки шаблонов. Так что тут так — и этим своим не станешь, и эти врагами объявятся)))

Но в std есть куча полезных вещей, которые с контейнерами не связаны — скажем, std:nth_element или std::atomic; контейнеры в отдельный неймспейс не выделили.


Про std:nth_element даже не знал, спасибо, неплохая вещь для поиска медиан.

Атомики жалко, да.

Меня лично спан (как и все остальные контейнеры STL) огорчает в основном тем, что он не проверяет выход за границы в операторе [] — а легко везде заменить [] на at — не особо получается.


Требования совместимости. Впрочем, выход за границы массива — вещь нечастая. А при наличии foreach… Который еще и оптимизируется лучше.

Но спан все равно лучше, чем отдельно указатель и размер


Имхо норм указатель и размер, я даже в шарпе этого не стесняюсь при работе со строками (иначе получается нечитаемая хтонь), просто дело же не только в этом, спан типа вообще лучше.

Вы Аду не пробовали? Тут параллельно идет обсуждение. Там все есть, и все красиво. :)
А вы про какую ситуацию? Просто какой-нибудь парсер со сложным автоматом на кучу потоков не заменишь. Или вы про другое?

Парсер не особо, но просто какую-то логику "событийную" типа "послать такой запрос — подождать ответ (или таймаут) — послать следующий — подождать" — иногда очень хочется.


Да, и оно ведь во многом растет от библиотеки шаблонов. Так что тут так — и этим своим не станешь, и эти врагами объявятся)))

На мой взгляд, оно растет чаще просто от незнания; многие думают, что в С++ память как-то "сама по себе тратится" и не пытаются вникать дальше, просто сходу отметают.


Требования совместимости. Впрочем, выход за границы массива — вещь нечастая. А при наличии foreach… Который еще и оптимизируется лучше.

Имхо норм указатель и размер, я даже в шарпе этого не стесняюсь при работе со строками (иначе получается нечитаемая хтонь), просто дело же не только в этом, спан типа вообще лучше.

foreach по указателю как раз не сделать, например.
Я лично регулярно за границы вылезал, прям фобия развилась. Особенно учитывая, что никаких санитайзеров-то нема, можно ведь вылезти и не заметить вообще.


Вы Аду не пробовали? Тут параллельно идет обсуждение. Там все есть, и все красиво. :)

Слышать — слышал, пробовать — не пробовал. В бывшем СНГ вроде на ней вакансий около нуля, так что… В Rust лично у меня веры больше.

Парсер не особо, но просто какую-то логику «событийную» типа «послать такой запрос — подождать ответ (или таймаут) — послать следующий — подождать» — иногда очень хочется.


Да, тут бы асинк, но и на потоках можно.

На мой взгляд, оно растет чаще просто от незнания; многие думают, что в С++ память как-то «сама по себе тратится» и не пытаются вникать дальше, просто сходу отметают.


Людей можно понять, в плюсах и функции «сами собой вызываются». Причем ведь накалываешься на мелочах — упустил буквально один символ при наборе, и если бы у тебя не был удален конструктор копирования, то ты бы не увидел ошибку, и возможно нескоро узнал бы, что делаешь что-то не то.

Я лично регулярно за границы вылезал, прям фобия развилась. Особенно учитывая, что никаких санитайзеров-то нема, можно ведь вылезти и не заметить вообще.


Хм, а я ни разу. А что с санитайзерами кстати, почему нет?
Людей можно понять, в плюсах и функции «сами собой вызываются». Причем ведь накалываешься на мелочах — упустил буквально один символ при наборе, и если бы у тебя не был удален конструктор копирования, то ты бы не увидел ошибку, и возможно нескоро узнал бы, что делаешь что-то не то.

Да я их прекрасно понимаю, че уж. С по крайней мере можно целиком в голове удержать, а С++ уже кажется нельзя :)


Хм, а я ни разу. А что с санитайзерами кстати, почему нет?

Почему — не знаю, просто нету их и все.
Ну, точнее, у Кейла они в альфе сейчас https://www.keil.com/support/man/docs/armclang_ref/armclang_ref_lnk1549304794624.htm, про другие тулчейны не в курсе

А, вы про кейл. Просто я сразу вспомнил про:
gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
github.com/google/sanitizers/wiki/AddressSanitizer

Как раз чтобы бороться с выходом за границы массива.

Да я их прекрасно понимаю, че уж. С по крайней мере можно целиком в голове удержать, а С++ уже кажется нельзя


Да-да-да… Там вон ниже гражданин возмущается, но я воздержусь от ответа, так как в общих чертах его уже знаю — «вам надо было написать программу совершенно иначе, и тогда не было бы проблем». Этим плюсы и «прекрасны» для любого человека на планете, кроме, вероятно, khim-а, что как бы вы ни написали ваш код на плюсах, где-то в последнем стандарте есть способ написать его более правильно.
А, вы про кейл. Просто я сразу вспомнил про:
gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
github.com/google/sanitizers/wiki/AddressSanitizer

А разве эти опции работают для таргета arm-none-eabi?

Сам не пробовал, не знаю. Просто вспомнил, что вот же они.

В последний раз я давненько проверял, конечно. Надо будет перепроверить, но чет я сомневаюсь.

Причем ведь накалываешься на мелочах — упустил буквально один символ при наборе, и если бы у тебя не был удален конструктор копирования, то ты бы не увидел ошибку, и возможно нескоро узнал бы, что делаешь что-то не то.

А можно подробнее и про удаленный конструктор копирования и пример одной забытой буквы? Спасибо.
Если вас это не обидит, то я бы воздержался, так как дискуссия будет взаимно бесполезной.
Да это не дискуссия, действительно интересно что это такое.
НЛО прилетело и опубликовало эту надпись здесь
Согласен, только проблемы могут вам оставить в наследство, их даже не придется самолично организовывать. Ну, и ничего героического в их решении уже нет, к сожалению.
О, а подскажите это случайно не та задача, что от международного производителя медицинского оборудования. Интеллектуальные капельницы («инфузоматы»). Они предназначены для внутривенного вливания пациенту растворов, лекарственных препаратов и питательных веществ в точно заданном объеме и с необходимой для этого скоростью?
Троллейбус.жпег

Отказаться от динамической аллокации ради надежности, потом искать сбои в рантайме, так как stdlib пересобрать нет возможности без исключений, а размер массива «эмпирически» вычисляется под shared_ptr. Потом ищи в какой фазе луны их стало мало.

Вы любите жить в кредит? Писать ПО в кредит точно у вас хорошо получается…
Да, жить в кредит совсем грустно, наверное.
Можно отказаться от исключений, но это не значит, что прошивка никогда не упадет. Но мы ни разу не видели сбоя в рантайме по причине проскочившего шального исключения. Мы соблюдали технику безопасности. Аналогично с умным указателем: разница в размере дополнительной информации образуется из-за разницы в реализации под конкретный компилятор. Эмпирический подбор состоял в том, что прошивка не собиралась, если памяти было мало. Хоть китайское электричество до конца не изучено, но тут от фазы луны мало что зависело. (upd: в настоящее время все shared_ptr удалены)
Мы никогда не полагаемся на удачу или UB.
Автор, пиши ещё! Стиль супер!
Интересно было бы в сравнение добавить Keil.
Спасибо! Я бы и рад, но пока Keil ускользает от меня.
Реализации есть, я видел несколько своими глазами. Но у всех есть фатальный недостаток. В таких проектах бывает очень сложно привлекать сторонние библиотеки. Телодвижений придется исполнить настолько много, что поневоле приходишь к мысли, что уж лучше свой моноцикл.
Не спорю, тут дело скорее во вяких бюрократических кунштюках
Т.е. люди пытаются в ограниченных условиях, вместо использования специально для этого созданных библиотек или подходов, использовать стандартные, предназначенные для абсолютно другого окружения, и после этого героически падать? Причем падать глупо и все время на лицо.

Ну… я даже не знаю что и сказать. Т.е. сами придумали себе проблем и сами их героически решаете — путь Дон Кихота не зарастет никогда. Где-то рядом промчался хлебный троллейбус.

А все потому, что думать — лень, что хочется все делать готовым микроскопом, даже если он не предназначен для забивания гвоздей. И иногда не стоит бездумно использовать ООП, обертки, исключения и другие достаточно тяжелые вещи там, где без этого вполне можно обойтись. И дело тут не в языке — ибо C++ отлично работает и без всего этого, а конкретно в том, кто пишет.

Мне страшно у вас спросить — у меня множество проектов на ATTINY85 как раз на C++ — а там всего 512 БАЙТ ОЗУ — как бы вы там std::array или std::vector-ом то пользовались и исключениями?

Мораль простая — не нужно использовать инструмент там, где он абсолютно не подходит, даже, если инструмент — стандартный и привычный. Нужно чуть потрудится и либо найти и использовать подходящий инструмент, или (О, БОЖЕ!) написать свой.

srd::array оверхед имеет только без оптимизации совсем. А так это то же самое что и голый массив.
Inline по оптимизации ставите и никаких отличий.
Зато пользоваться значительно безопаснее.
Вектор согласен, если заранее известен размер, непонятен смысл его использования.
Эксепшены, имхо, одни минусы. Мало того, что оверхед гигантский, так еще и SIL не рекомендует их и с ними пройти сертификацию практически невозможно.

К сожалению стандарт не требует размер std::array чтобы был равен размеру массива.
sizeof(array<int,10>) >= sizeof(int[10])

А зачем sizeof ом выяснять размер массива? Это не безопасно, указатель передадите и вот вам уже размер указателя.
Для этого есть std::size. Там все будет равно.

Всё таки это абсолютно разные вещи:
sizeof — возвращает размер в char-ах
std::size — возвращает количество элементов
Для этого есть std::size. Там всё будет равно.
Метод std::size для «голых» указателей вообще не определён.

Да, поэтому непонятно зачем sizeof использовать для массивов.
Вы же знаете тип, знаете количество элементов, можете получить размер в байтах.
Имхо, sizeof имеет смысл использовать только для типов.


Метод std::size для «голых» указателей вообще не определён

Для "голых" массивов вполне даже определен.

Да, поэтому непонятно зачем sizeof использовать для массивов.
А затем, что вам таким образом хотят сказать что есть накладные расходы у std::array по сравнению с голым массивом:
sizeof(array<int,10>) >= sizeof(int[10])
А количество элементов и там и там будет равным:
std::size(array<int,10>) == std::size(int[10])
Для «голых» массивов вполне даже определен
А при чем тут голые массивы, вы же рассматривали указатели.
sizeof(array<int,10>) >= sizeof(int[10])
Это классическая перестраховочное допущение. На практике я не вижу причины, почему не должно выполняться равенство. Или может у Вас они есть?
Это не ко мне, а к автору утверждения. Я только ответил на вопрос почему иcпользуется sizeof, а не std::size.

Я понял, вы имеете ввиду, что std::array хранит в себе size. Но там же все constexpr


  constexpr size_type size() const _NOEXCEPT
  {       // return length of sequence
    return _Size;
  }

т.е. компилятор фактически все обсчитает на этапе компиляции и ничего хранить не будет. Возможно стандарт разрешает делать размер std::array больше размера голого массива, но эта такая еще реализация должны быть. Мне такие не встречались.
Даже проверил специально на IAR 8.40.2



Как говориться найди отличия.
Поэтому не пойму, где там оверхед должен быть?

Я думаю что любая вменяемая реализация в практических случаях будет давать на выходе размер такой же как у «сырого» массива. Мне трудно сказать почему стандарт задает отношение >= по размеру и какой частный случай реализации имеется в виду. Может быть имеется вполне валидный случай std::array<type_t, 0>, когда размер std::array будет все-таки больше 0, хотя выражение с сырым массивом type_t a[0] недопустимо. В любом случае, стандарт стоит учитывать и если размер так важен, то лучше поставить static_assert.
Вот еще нарыл обсуждение на эту тему. В определенных реализациях может присутствовать выравнивание, т.е. в случае std::array<int, 10> array; sizeof(std::array<int, 10>) может быть sizeof(int) * std::size(array) + P

Ага, понял, спасибо.

К сожалению, оверхед там есть, пусть и небольшой, но все это в конце-концов копится.

Однако дело даже не в этом, дело в том, что в 90% случаев вполне можно обойтись обычным массивом, не прибегая к обертке. Это не значит, что так надо делать везде и всегда, все зависит от задачи. Но в случае ограниченности памяти это вполне уместно.

Насчет безопасности — да, безопасней, однако, если программист понимает, как оно устроено в памяти и понимает зачем ему нужен именно массив (а разработчик встройки просто обязан это знать и понимать), то большинство проблем с безопасностью также отпадает — достаточно понимать граничные случаи. Если же они достаточно специфичны — можно обернуть все это в класс, где все их учесть — и по оверхэду это все равно будет меньше, т.к. системные обертки написаны со всеми возможными и невозможными допущениями, которые в данном окружении сильно избыточны.
А можно пример проявления оверхеда в std::array<T, N>?

https://m.habr.com/ru/company/auriga/blog/539760/#comment_22898618


Вполне возможно, что в простых случаях компилятор всё оптимизирует, но нет требований, чтобы это было всегда.

Да, проверил в MSVC — размер std::array<int, 0> равен 4-ем байтам. С другой стороны, я знаю лишь один вариант использования массивов нулевой длины, и в этом случае std::array не подходит в принципе, нужен нативный массив.
В остальном — вообще на много чего нет гарантий. На то что компилятор не вставит Sleep(1000) после каждой инструкции — тоже гарантии нет. Но вообще он не вставляет.
Вот бы было указано в стандарте, что std::array должен быть двоично идентичен C-array тогда можно было бы использовать std::array не боясь неправильного размещения в памяти.
А так его можно использовать только там где это не критично.

Кстати, у std::array есть ещё одна неприятная особенность.
std::array::size имеет тип size_t, а sizeof(a)/sizeof(a[0]) это константа, которую компилятор легко превращает в нужный тип.
Итого в MSVC получаем предуреждение об усечении типа:

std::array<int, 1> a;
short size_a = a.size(); // warning C4267: 'initializing': conversion from 'size_t' to 'short', possible loss of data

int b[1];
short size_b = sizeof(b)/sizeo(b[0]); // OK
В C++ в принципе с массивами нулевой длины есть неудобства. Они вызывают варнинг, который мне всегда приходится отключать прагмой, когда объявляется такой массив. Насколько я понимаю, это из-за того, что в C++ объект не может иметь нулевой размер.
На счет size_t — ну так это правильный тип для размера в памяти, надо приведение — просто пару букв дописать. А вот sizeof(a)/sizeof(a[0]) выглядит архаично, так и не скажешь, дружит ли оно с выравниванием, ну и есть это:
template<typename T, uptr N>
inline constexpr uptr array_size( T ( & )[N] )
{
    return N;
}
Конечно в коде не будет так, а будет макрос "_countof", который и проверяет тип на массив.
Легко сказать, что нужно приведение типа каждый раз, но это банально неудобно.
sizeof(a)/sizeof(a[0]

Тоже тип будет size_t, так как sizeof — возращает size_t. И ОК вам выдает, только встроенная диагностика, а любой анализатор, типа PCLint, так же руганется там и по правильному в обоих случаях к short привести явно. Если вообще этот short тут нужен.
А в строчке с warning все верно компилятор написал, сделайте


constexpr auto size_a = a.size()

Какая идея у преобразования size в short? Либо явно тогда преобразуйте, либо оставьте тип size_t. Память сэкономить? Но тогда у вас доступ может быть медленнее.

Бывают такие функции, которые принимают размер не в size_t :)

Тогда нужно в любом случае явно кастить, иначе у вас чисто теоретически в другом проекте может быть косяк.
Дело в том, что short size_a = a.size() — это рантйм проверка, и поэтому вам анализатор выдает, что чисто теоретически, если вдруг вы когда-то зададите массив размеров больше длины типа short, тут будет обрезка.


А sizeof(b)/sizeo(b[0]) — это не в реальном мире — это во время компиляции, и анализатор точно значет, что для вашего b, это не выйдет за размер short.


чтобы убрать ворнинг, думаю можете сделать так:


constexpr short size_a = a.size();

Тогда компилятор будет высчитывать это на этапе компиляции и поймет, что вы не вышли за границы типа short.


но правильно делать кончено так:


constexpr short size_a = static_cast<short>(a.size());
Без приведения типа будет предупреждение даже с constexpr: gcc.godbolt.org/z/EeWsGna3E

А с массивом С эти приседания не нужны.

Ок, не нужны, если использовать sizeof.
Зато прикольно будет, если кто-то потом сделает так:
https://gcc.godbolt.org/z/YEMd6hfa5


В первом случае вас предупредили, что возможно потенциальная ошибка, и обратили ваше внимание, на то, а точно ли вы хотите, чтобы так было? Ну если точно, то вы должны сделать cast. А если не точно, то значит вы так не хотели, и просто ошиблись.

Так компилятор же вам и сообщает в вашем примере, что что-то не то:

(5): warning C4305: 'initializing': truncation from 'unsigned __int64' to 'short'
<source>(5): warning C4309: 'initializing': truncation of constant value
</code>

Мы ведь не игнорируем предупреждения компилятора, не так ли ? ;)

Все верно, я и говорю, сурпрайз будет… что функцию больше не будет работать.
В первом случае вам просто об этом сказали чуточку раньше, чтобы бы уверенно сказали, что ДА, я так хочу.

Вот бы было указано в стандарте, что std::array должен быть двоично идентичен C-array тогда можно было бы использовать std::array не боясь неправильного размещения в памяти.
Я тебе одну умную вещь скажу, только ты не одижайся. Но для С-array тоже не гарантируется «неправильное размещение в памяти» если под «неправильным» понимается выравнивание. Только в случае с массивом выравнивание сделает менеджер памяти и вы это ни когда не увидите. Вы реально считаете, что запрашивая new char[3] будет выделен блок памяти в три байта?
Кстати, у std::array есть ещё одна неприятная особенность.
std::array::size имеет тип size_t
И это как раз правильно.
sizeof(a)/sizeof(a[0]) это константа, которую компилятор легко превращает в нужный тип
Тут компилятор подставляет неявное преобразование, это действительно правильное поведение?

ага я не понял, откуда там оверхед?
вот я на IAR проверил


В случае конкретно std:array оверхеда нет, а если в одной реализации имеет место аллокация большего объёма памяти чем для массива, всегда можно изменить на другую, где оверхеда не будет.
Советую посмотреть следующее видео с 8 по 23 минуты, там как раз как можно эффективно применять С++ во встройке для валидации очень многих вещей на этапе компиляции и получить ровно такой же бинарник, как и на С в конце, только написание кода на порядок безопаснее.
youtu.be/TYqbgvHfxjM
если программист понимает, как оно устроено в памяти и понимает зачем ему нужен именно массив, то большинство проблем с безопасностью также отпадает

Да-да — сначала программист умный и всё понимает, а потом внезапно heartbleed.
Одни знакомые парни делали проект под xmega128, естественно на плюсах, и там нужно было сделать меню на LCD индикаторе. Они сделали небольшое меню и всё было хорошо. Но когда меню потребовалось сильно увеличить, тут внезапно выяснилось, что 8Кб RAM это очень мало, при этом там полно других задач, на которые тратится память. А ведь старые саксаулы им говорили как нужно делать.

А какое это отношение имеет к языку и при чём тут кусты?

Они там сначала всякое C++42 хипстерство навернули от недостатка опыта на железе

Ещё раз, а при чём тут С++42 если проблема "от недостатка опыта на железе"

При правильном на подходе С++ можно накрутить намного больше чем на чистом Си, с кучей валидаций на этапе компиляции и синтаксического сахара, но итоговый бинарник будет ровно таким же как и на Си.

https://youtu.be/TYqbgvHfxjM

тут на 22:30 есть сравнение итогового ассемблерного кода

А я и не писал что дело в С++. Дело в том, что нужно знать что и где применять. А если не знать, то может получиться грустно.
А с точки зрения всяких DIY проектов, что выходит лучше сидеть на си и не рыпаться?
Рискну предположить, что большинство и профессиональных эмбед-проектов вполне себе неплохо сидят на си и не жалуются.
Кресты в любительский эмбед активно привнесло ардуино, насколько помню.
Мне кажется, не стоит себя так ограничивать. Даже с точки зрения DIY
плюсы таки удобнее и безопаснее. Да и вообще, «это красиво». (Эдю не слушать — это жызнелюб по сути… ;) )
Да и вообще. Вот скажем, есть задача: кучка шаговиков которыми нужно рулить.
С:
Упаковываем пины в структуры, пишем процедуы управления, но когда нужно сделать шаг(И) — как передать экземпляр? Понятно что указателем на структуру конкретного ШД. Т.е. нечто вроде steps(*leftStepper, 10)
при этом все руками описывать и ни дай бог чтонить изменить в сигнатурах — придеццо перелопатить весь исходник.
плюсы:
LeftStepper.(тут выпадаете автодополнение на методы)steps(10);
при изменении структуры данных или методов — правки минимальны обычно…

плюс таки сильнее контроль типов, шаблоны, constexpr, вот это вот все…
Автодополнения может не быть, либо такое, чтобы его лучше не было. Занятно, но почему-то интеллисенс студийный — какой-то оверхайтек, так сложно его повторить, просто не для средних умов…

На самом деле ваше сравнение неудачное имхо, принципиальных отличий нет, вызываете ли вы foo(*bar, baz...) или bar.foo(baz...). Как по мне, кратно более крутое отличие, понятное широкой аудитории — наличие неймспейсов в плюсах.
Может и неудачное, но меня просто вымораживало когда писал управление несколькими шаговиками вот именно это место, а плюсы низзяя было… %)
а принципиальное отличие… left[автодополнение].s[автодополнение]([выпадает нужный список параметров, заполняем]); гораздо быстрее.
В принципе да, встречается такое автодополнение что лучше бы его не было, но ведь всегда можно взять внешний редактор который вот прям как надо.
Справедливости ради, описанное ООП вполне себе штатно реализуется и на чистом си через структуру с указателями на методы, рукописный вариант vtable из плюсов.
Угу. И все руками, руками… А чего ради?
Именно для этого и была придумана Ада.
Вместо тяжеловесной std::function зачастую возможно использовать более легковесные non-owning non-allocating аналоги, например llvm::function_ref из проекта LLVM.
Ни разу не потребовалось в embedded программинге ни динамическая аллокация, ни исключения, ни умные указатели. Вся память выделяется статически на этапе компиляции. Структуры данных в основном — циклические буфера фиксированного размера, кратного степени 2. Ну или просто некий глобальный объект, в котором в структурном виде собраны все переменные, описывающие состояние системы.

Иногда приходится использовать библиотеки, которые кучу используют (хоть и позиционируются как embedded), например, ST этим регулярно грешат.
Или, скажем, lwip — да, pbuf'ы выделяются не в обычной куче, а в пуле, но это решает только проблему с фрагментацией, а память как текла, так и течет :)

Оно работает ровно до тех пор, пока не поймёшь, что стандартные контейнеры не всегда вызывают аллокаторы что бы выделить память. Точнее, для выделения памяти под элементы — да, вызывают, а для внутренних данных могут вызывать как прямо new/delete, так и в принципе что-то своё. Раньше этим std::list грешил, так как кроме хранимого элемента нужно ещё память для структуры с указателями выделить.

Переопределить глобальные операторы new и delete?

Тогда можно и с аллокатором. не возиться :)
Вот очередной пример гребаного трэша, если честно. То есть эмбеддер должен пойтить и почитать про их переопределение, а судя по статьям на Хабре сие есть тема непростая, если глубоко копать.

Кто-нибудь сейчас читает и думает — ага, норм, шаблоны, классы, операторы можно переопределять, просто мякотка. Только сперва надо язык обрезать садовыми ножницами, которых притом нет… Ой, да ну его…

"C++ is not hard, it's just expert-friendly" :)

Родить свой аллокатор с поправкой на микроконтроллер не так уж сложно, делал.

Наконец-то появилось время внимательно прочитать.


Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось

На самом деле есть, все описано в руководстве на С++, в разделе "Overview—Standard C++", а баг с биндингом вынесен мной еще года 2 или 3 назад и так и висит у них тут:
[EWARM-7305, TPB-3270]


В принципе даже со сложными темплейтными шаблона он справляется на ура. Есть пару небольших косяков еще, например невозможно использовать if constexpr в конструкторе, но это они поправили в 8.50 версии. Ну и по мелочам, но в целом все неплохо.


std::exception. Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

Там почти на всей библиотеки стоит noexcept, так что правильно, что не используете exception — офигенный оверхед, недопустимо использовать для SIL и вообще можно обойтись из них, преимущества их использования для встроенного ПО вообще мне непонятны.


std::vector

Про вектор, я уже написал, если вы задаете ограниченный размер, под вектор, который все равно вам известен на этапе компиляции и никогда не поменяется. Смысл использовать вектор вообще не понятен.


std::shared_ptr

Тоже самое, что и с вектором — стреляем из пушки по воробьям. Самое интересное, что для сертификации по SIL вам придется описывать все это в архитетктуре, писать юнит тесты, исправлять замечания статического анализатора, и все для того, чтобы использовать то, что использовать не нужно. Т.е. вы просто добалвяете ненужных еффортов на разработку и сертификацию. Странно, только если уже так был написан легаси код, и в не хотите его менять.


В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

Вот тут не понял, какая еще динамическая эллокация? Тут же все известно на этапе компиляции, максимум memset c известным размером на стеке. Heap тут точно не будет. Надо её в настройках проекта в 0 ставить, чтобы проверить, что у вас нигде нет динамической эллокации.


std::function явно вызываются исключения,

Это явно не про IAR, там все вот такое:


function() _NOEXCEPT
  { // construct empty function wrapper
  }

  function(_Unutterable) _NOEXCEPT
  { // construct empty function wrapper from null pointer
  }

Поэтому IAR и является стандартом для SIL ембеддед разработки, что все эти ненужные вещи типа rti, exception можно отключить и ваша библиотека будет работать без всех этих ненужных вещей. А вот GCC универсальная штука, которая на ебмед болт положила, отсюда и запрет на его использование для SIL.


static auto global_handlers = std::pair<Handler, Handler> {};

Да такая же ситуация случается с constexpr константами, когда кто-то хочет обратиться по их адресам, каждый в разных юнитсах компиляции получает разные адреса. В итоге код быстро разрастается, поэтому inline необходим!


Вообще у IAR есть 8.40.3 Functional Safety — на основе 8.40.2 — работает хорошо, а исключением известных багов, которые хорошо описаны.

С вами всегда приятно поспорить, так что с вами и поспорю в единственный выходной на этой неделе. :)

правильно, что не используете exception — офигенный оверхед, недопустимо использовать для SIL и вообще можно обойтись из них, преимущества их использования для встроенного ПО вообще мне непонятны


Несмотря на бесспорность утверждения, я как-то в разговоре с одним коллегой пришел к обратному выводу. Представьте себе, что при вычислениях, при каком-нибудь банальном сложении, что-то пошло не так и (если вам повезло и у вас локстепнутое ядро) вывалилось исключение процессора. Вы можете внутри него просто передать управление на резервный комплект, а тут уйти в перезагрузку и ресинхронизацию. А можете в его коде применить разнообразную эвристику (типа пройтись по стеку например) и вернуться обратно в кодец и например заново пройти данный цикл вычислений, пересинхронизировавшись с другим комплектом без перезагрузки (соответственно коэффициент готовности растет). А если его нет, или если потеря точности в один цикл несущественна (например вы там сидите и интегрируете дифур) — так и пойти дальше. И вот когда есть исключения и код обернут верно, то вы получите инфу об этом в нужном месте основного кода, без сложных способов возврата из обработчика, дропнете этот цикл и все.

Недопустимость оных для SIL sensitive приложений вообще веселая штука. Вот есть в Аде обработчики исключительных ситуаций процедур, по сути те же самые, но их не только можно, а нужно использовать…

Смысл использовать вектор вообще не понятен


Положим вам нужен массив переменной длины с постоянным размером в памяти. Для std::array потребуется отдельная переменная длины, а range based for вообще не взлетит. Вот отсюда и рождается вектор на некоем заранее заданном хранилище.

Тоже самое, что и с вектором — стреляем из пушки по воробьям


А почему? И это, разве есть способ никогда-никогда не использовать shared_ptr?
А почему? И это, разве есть способ никогда-никогда не использовать shared_ptr?
Да, использовать unique_ptr, в большинстве случаев его более чем достаточно и нет оверхеда с подсчётом ссылок.
Стоп-стоп, ну конечно, если вам не нужно расшаривание, то не нужен и shared_ptr. А если нужно?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий