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


AliExpress RU&CIS

C++ is a horrible language. It's made more horrible by the fact that a lot of substandard programmers use it, to the point where it's much much easier to generate total and utter crap with it.

Linus Benedict Torvalds

Собеседование шло уже второй час. Мы наконец-то закончили тягучее и вязкое обсуждение моей скромной персоны, и фокус внимания плавно переполз на предлагаемый мне проект. Самый бойкий из трех моих собеседников со знанием дела и без лишних деталей принялся за его описание. Говорил он быстро и уверенно – явно повторяет весь этот рассказ уже не первый раз. По его словам, работа велась над неким чрезвычайно малым, но очень важным устройством на базе STM32L4. Потребление энергии должно быть сведено к минимуму... USART... SPI... ничего необычного, уже неоднократно слышал подобное. После нескольких убаюкивающих фраз собеседник внезапно подался чуть вперед и, перехватив мой сонный взгляд, не без гордости произнес:

— А firmware мы пишем на C++! – мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

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

— У вас есть какие-то опасения? – поспешил спросить он с искренней озабоченностью в голосе.

Опасения у меня всегда имеются в избытке. Я с ностальгическим умилением вспомнил, как писал первую прошивку на MbedOS для одной из демонстрационных плат. Память тогда закончилась быстрее, чем я успел моргнуть светодиодом второй раз. Эх, опыт, сын ошибок... Надо все же признать, что страшные темные времена, когда о прошивке на «плюсах» никто и не заикался, давно прошли. Все разумные возражения в настоящее время рассыпались в труху и превратились в мифы. Вроде бы...

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

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

— И у нас есть код ревью! – встрепенувшись, поспешили добавить хором двое других невероятно квалифицированных членов команды.

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

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", – наивно думал я, – "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько ни уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала «Игры престолов».

Можно справедливо заметить, что всему виной сложные шаблонные конструкции. Да, но у GCC с пониманием аналогичных шаблонов никогда не было проблем.

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level – уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

Проще говоря, если от вашего устройства зависят жизни людей, то при его разработке нужно придерживаться определенных правил. Одно из них – это отсутствие динамического распределения памяти, по крайней мере, после вызова функции main. Думаю, все знают, что в no-OS устройствах динамические аллокации чреваты проблемами, вроде фрагментации памяти и т.п.

Одно только это правило привело ко всем последующим значительным изменениям в коде, о которых речь пойдет ниже.

std::exception

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

Не будем вдаваться в подробности, как именно обрабатываются исключения в С++, полагаю, это всем известно и так. Если разложить процесс на элементарные функции, то можно найти среди них такую:

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе. Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать осторожней вдвойне, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

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

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

template <class T, std::size_t Size>
class StaticArray {
 using ssize_t = int;

public:
 using value_type = T;
 template <class U>
 struct rebind {
   using other = StaticArray<U, Size>;
 };
 StaticArray() = default;
 ~StaticArray() = default;
 template <class U, std::size_t S>
 StaticArray(const StaticArray<U, S>&);

 auto allocate(std::size_t n) -> value_type*;
 auto deallocate(value_type* p, std::size_t n) -> void;
 auto max_size() const -> std::size_t;
};

Ключевые функции, конечно, allocate и deallocate. Передаваемый им параметр n это не размер в байтах, а размер в попугаях, которые хранятся в векторе. Функция max_size используется при проверке вместимости аллокатора и возвращает максимально возможное теоретически число, которое можно передать в функцию allocate.

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;
    
v.push_back(1000);
std::cout<<"check size "<<v.size()<<std::endl;
    
v.push_back(2000);
std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

Конечно, контролировать управление памятью путем использования кастомных аллокаторов вполне возможно. В стандартной библиотеке есть замечательная функция std::allocate_shared, которая создаст разделяемый объект именно там, где мы укажем. Указать же можно самолепным аллокатором примерно такого вида:

template <class Element, 
          std::size_t Size, 
          class SharedWrapper = Element>
class StaticSharedAllocator { 
 public:
  static constexpr std::size_t kSize = Size;
  using value_type = SharedWrapper;
  using pool_type = StaticPool<Element, kSize>;
  pool_type &pool_;
  using ElementPlaceHolder = pool_type::value_type;

  template <class U>
  struct rebind {
    using other = StaticSharedAllocator<Element, kSize, U>;
  };

  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}
  ~StaticSharedAllocator() = default;
  template <class Other, std::size_t OtherSize>
  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other) 
    : pool_{other.pool_} {}

  auto allocate(std::size_t n) -> value_type * {
    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));
    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));
    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);
  
    return reinterpret_cast<value_type *>(pool_.allocate(n));
  }

  auto deallocate(value_type *p, std::size_t n) -> void {
    pool_.deallocate(reinterpret_cast<value_type *>(p), n);
  }
};

Очевидно, Element – тип целевого объекта, который и должен храниться как разделяемый объект. Size – максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper – это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

Лично я не нашел ничего лучше, чем просто резервировать немного больше места в хранилище или пуле объектов. Размер дополнительного пространства подобран эмпирически, ибо разным компиляторам нужен разный объем дополнительной памяти.

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

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>
struct StaticPool {
  static constexpr size_t kSize = Size;
  static constexpr size_t kSizeOverhead = 48;
  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead, 
                                            alignof(std::max_align_t)>;
  StaticArray<value_type, Size> pool_;
  
  auto allocate(std::size_t n) -> value_type * {
    return pool_.allocate(n);
  }
  auto deallocate(value_type *p, std::size_t n) -> void {
    pool_.deallocate(p, n);
  }
};

А теперь небольшой пример, как это все работает вместе:

struct Object {
  int index;
};
constexpr size_t kMaxObjectNumber = 10u;

StaticPool<Object, kMaxObjectNumber> object_pool {};

StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};

std::shared_ptr<Object> MakeObject() {
  return std::allocate_shared<Object>(object_alloc_);
}

Все эти изуверства позволили избежать динамического выделения памяти и здесь. Хотя, мне кажется, лучше реорганизовать код так, чтоб не было необходимости использовать shared_ptr вообще. К сожалению, не всегда рефакторинг можно сделать быстро и безболезненно.

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};
    auto sum = [=] () -> int {
      int sum = x[0];
      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {
        sum += x[i];
      }
      return sum;
    };
    
    std::function<int()> callback = sum; 

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

Дело в том, что в классе нашей универсальной обертки содержится небольшой участок памяти (place holder), где может быть определена содержащаяся функция.

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка – стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

?text

?data

?bss

67 880

2 496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

Внутри std::function явно вызываются исключения, которые внутри себя используют demangling для имен функций, и это дорогое удовольствие. Нужно ли это в небольших устройствах? Скорее всего, любоваться красивыми именами не придется.

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440	cplus_demangle_operators
0000049e	__gxx_personality_v0
000004c4 	d_encoding
000004fe	d_exprlist
00000574	_malloc_r
0000060c	d_print_mod
000007f0	d_type
00000eec	_dtoa_r
00001b36	_svfprintf_r
0000306c	d_print_comp

Много функций с префиксом d_* – функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free – функции для работы с динамическим выделением памяти.

Результат ожидаемый – флаги -fno-exceptions и -fno-rtti не спасают.

Внедрим второй подобный функциональный объект в другой единице трансляции:

?text

?data

?bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

Показательно также то, сколько объектных файлов и из каких библиотек мы используем для этих случаев.

Для случая без std::function список короткий
libc_nano.a
libg_nano.a
libg_nano.a(lib_a-exit.o)
libg_nano.a(lib_a-exit.o) (_global_impure_ptr)
libg_nano.a(lib_a-impure.o)
libg_nano.a(lib_a-init.o)
libg_nano.a(lib_a-memcpy-stub.o)
libg_nano.a(lib_a-memset.o)
libgcc.a
libm.a
libstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.a
libg.a
libg.a(lib_a-__atexit.o)
libg.a(lib_a-__call_atexit.o)
libg.a(lib_a-__call_atexit.o) (__libc_fini_array)
libg.a(lib_a-__call_atexit.o) (atexit)
libg.a(lib_a-abort.o)
libg.a(lib_a-abort.o) (_exit)
libg.a(lib_a-abort.o) (raise)
libg.a(lib_a-atexit.o)
libg.a(lib_a-callocr.o)
libg.a(lib_a-closer.o)
libg.a(lib_a-closer.o) (_close)
libg.a(lib_a-ctype_.o)
libg.a(lib_a-cxa_atexit.o)
libg.a(lib_a-cxa_atexit.o) (__register_exitproc)
libg.a(lib_a-dtoa.o)
libg.a(lib_a-dtoa.o) (_Balloc)
libg.a(lib_a-dtoa.o) (__aeabi_ddiv)
libg.a(lib_a-exit.o)
libg.a(lib_a-exit.o) (__call_exitprocs)
libg.a(lib_a-exit.o) (_global_impure_ptr)
libg.a(lib_a-fclose.o)
libg.a(lib_a-fflush.o)
libg.a(lib_a-findfp.o)
libg.a(lib_a-findfp.o) (__sread)
libg.a(lib_a-findfp.o) (_fclose_r)
libg.a(lib_a-findfp.o) (_fwalk)
libg.a(lib_a-fini.o)
libg.a(lib_a-fputc.o)
libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)
libg.a(lib_a-fputc.o) (__sinit)
libg.a(lib_a-fputc.o) (_putc_r)
libg.a(lib_a-fputs.o)
libg.a(lib_a-fputs.o) (__sfvwrite_r)
libg.a(lib_a-freer.o)
libg.a(lib_a-fstatr.o)
libg.a(lib_a-fstatr.o) (_fstat)
libg.a(lib_a-fvwrite.o)
libg.a(lib_a-fvwrite.o) (__swsetup_r)
libg.a(lib_a-fvwrite.o) (_fflush_r)
libg.a(lib_a-fvwrite.o) (_free_r)
libg.a(lib_a-fvwrite.o) (_malloc_r)
libg.a(lib_a-fvwrite.o) (_realloc_r)
libg.a(lib_a-fvwrite.o) (memchr)
libg.a(lib_a-fvwrite.o) (memmove)
libg.a(lib_a-fwalk.o)
libg.a(lib_a-fwrite.o)
libg.a(lib_a-impure.o)
libg.a(lib_a-init.o)
libg.a(lib_a-isattyr.o)
libg.a(lib_a-isattyr.o) (_isatty)
libg.a(lib_a-locale.o)
libg.a(lib_a-locale.o) (__ascii_mbtowc)
libg.a(lib_a-locale.o) (__ascii_wctomb)
libg.a(lib_a-locale.o) (_ctype_)
libg.a(lib_a-localeconv.o)
libg.a(lib_a-localeconv.o) (__global_locale)
libg.a(lib_a-lock.o)
libg.a(lib_a-lseekr.o)
libg.a(lib_a-lseekr.o) (_lseek)
libg.a(lib_a-makebuf.o)
libg.a(lib_a-makebuf.o) (_fstat_r)
libg.a(lib_a-makebuf.o) (_isatty_r)
libg.a(lib_a-malloc.o)
libg.a(lib_a-mallocr.o)
libg.a(lib_a-mallocr.o) (__malloc_lock)
libg.a(lib_a-mallocr.o) (_sbrk_r)
libg.a(lib_a-mbtowc_r.o)
libg.a(lib_a-memchr.o)
libg.a(lib_a-memcmp.o)
libg.a(lib_a-memcpy.o)
libg.a(lib_a-memmove.o)
libg.a(lib_a-memset.o)
libg.a(lib_a-mlock.o)
libg.a(lib_a-mprec.o)
libg.a(lib_a-mprec.o) (_calloc_r)
libg.a(lib_a-putc.o)
libg.a(lib_a-putc.o) (__swbuf_r)
libg.a(lib_a-readr.o)
libg.a(lib_a-readr.o) (_read)
libg.a(lib_a-realloc.o)
libg.a(lib_a-reallocr.o)
libg.a(lib_a-reent.o)
libg.a(lib_a-s_frexp.o)
libg.a(lib_a-sbrkr.o)
libg.a(lib_a-sbrkr.o) (_sbrk)
libg.a(lib_a-sbrkr.o) (errno)
libg.a(lib_a-signal.o)
libg.a(lib_a-signal.o) (_kill_r)
libg.a(lib_a-signalr.o)
libg.a(lib_a-signalr.o) (_getpid)
libg.a(lib_a-signalr.o) (_kill)
libg.a(lib_a-sprintf.o)
libg.a(lib_a-sprintf.o) (_svfprintf_r)
libg.a(lib_a-stdio.o)
libg.a(lib_a-stdio.o) (_close_r)
libg.a(lib_a-stdio.o) (_lseek_r)
libg.a(lib_a-stdio.o) (_read_r)
libg.a(lib_a-strcmp.o)
libg.a(lib_a-strlen.o)
libg.a(lib_a-strncmp.o)
libg.a(lib_a-strncpy.o)
libg.a(lib_a-svfiprintf.o)
libg.a(lib_a-svfprintf.o)
libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)
libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)
libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)
libg.a(lib_a-svfprintf.o) (__aeabi_dmul)
libg.a(lib_a-svfprintf.o) (__aeabi_dsub)
libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)
libg.a(lib_a-svfprintf.o) (__ssprint_r)
libg.a(lib_a-svfprintf.o) (_dtoa_r)
libg.a(lib_a-svfprintf.o) (_localeconv_r)
libg.a(lib_a-svfprintf.o) (frexp)
libg.a(lib_a-svfprintf.o) (strncpy)
libg.a(lib_a-syswrite.o)
libg.a(lib_a-syswrite.o) (_write_r)
libg.a(lib_a-wbuf.o)
libg.a(lib_a-wctomb_r.o)
libg.a(lib_a-writer.o)
libg.a(lib_a-writer.o) (_write)
libg.a(lib_a-wsetup.o)
libg.a(lib_a-wsetup.o) (__smakebuf_r)
libgcc.a
libgcc.a(_aeabi_uldivmod.o)
libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)
libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)
libgcc.a(_arm_addsubdf3.o)
libgcc.a(_arm_cmpdf2.o)
libgcc.a(_arm_fixdfsi.o)
libgcc.a(_arm_muldf3.o)
libgcc.a(_arm_muldivdf3.o)
libgcc.a(_arm_unorddf2.o)
libgcc.a(_dvmd_tls.o)
libgcc.a(_udivmoddi4.o)
libgcc.a(libunwind.o)
libgcc.a(pr-support.o)
libgcc.a(unwind-arm.o)
libgcc.a(unwind-arm.o) (__gnu_unwind_execute)
libgcc.a(unwind-arm.o) (restore_core_regs)
libm.a
libnosys.a
libnosys.a(_exit.o)
libnosys.a(close.o)
libnosys.a(fstat.o)
libnosys.a(getpid.o)
libnosys.a(isatty.o)
libnosys.a(kill.o)
libnosys.a(lseek.o)
libnosys.a(read.o)
libnosys.a(sbrk.o)
libnosys.a(write.o)
libstdc++.a
libstdc++.a(atexit_arm.o)
libstdc++.a(atexit_arm.o) (__cxa_atexit)
libstdc++.a(class_type_info.o)
libstdc++.a(cp-demangle.o)
libstdc++.a(cp-demangle.o) (memcmp)
libstdc++.a(cp-demangle.o) (realloc)
libstdc++.a(cp-demangle.o) (sprintf)
libstdc++.a(cp-demangle.o) (strlen)
libstdc++.a(cp-demangle.o) (strncmp)
libstdc++.a(del_op.o)
libstdc++.a(del_ops.o)
libstdc++.a(eh_alloc.o)
libstdc++.a(eh_alloc.o) (std::terminate())
libstdc++.a(eh_alloc.o) (malloc)
libstdc++.a(eh_arm.o)
libstdc++.a(eh_call.o)
libstdc++.a(eh_call.o) (__cxa_get_globals_fast)
libstdc++.a(eh_catch.o)
libstdc++.a(eh_exception.o)
libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))
libstdc++.a(eh_exception.o) (__cxa_pure_virtual)
libstdc++.a(eh_globals.o)
libstdc++.a(eh_personality.o)
libstdc++.a(eh_term_handler.o)
libstdc++.a(eh_terminate.o)
libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)
libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)
libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())
libstdc++.a(eh_terminate.o) (__cxa_begin_catch)
libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)
libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)
libstdc++.a(eh_terminate.o) (__gxx_personality_v0)
libstdc++.a(eh_terminate.o) (abort)
libstdc++.a(eh_throw.o)
libstdc++.a(eh_type.o)
libstdc++.a(eh_unex_handler.o)
libstdc++.a(functional.o)
libstdc++.a(functional.o) (std::exception::~exception())
libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)
libstdc++.a(functional.o) (operator delete(void*))
libstdc++.a(functional.o) (__cxa_allocate_exception)
libstdc++.a(functional.o) (__cxa_throw)
libstdc++.a(pure.o)
libstdc++.a(pure.o) (write)
libstdc++.a(si_class_type_info.o)
libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)
libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)
libstdc++.a(tinfo.o)
libstdc++.a(tinfo.o) (strcmp)
libstdc++.a(vterminate.o)
libstdc++.a(vterminate.o) (__cxa_current_exception_type)
libstdc++.a(vterminate.o) (__cxa_demangle)
libstdc++.a(vterminate.o) (fputc)
libstdc++.a(vterminate.o) (fputs)
libstdc++.a(vterminate.o) (fwrite)

А что IAR?

Все устроено немного иначе. Он не требует явного указания спецификации nano или nosys, ему не нужны никакие заглушки. Этот компилятор все знает и сделает все в лучшем виде, не нужно ему мешать.

?text

?ro data

?rw data

2 958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o                                                     1'404                        496

heaptramp0.o                                                     4

Конечно же, появились функции и данные для работы с динамической памятью, как в случае GCC.

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

Объектный файл того юнита, в котором была добавлена функция, тоже ощутимо располнел:

до

main.cpp.obj                                           3'218               412   36'924

после

main.cpp.obj                                           4'746               451   36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

?text

?ro data

?rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

Убедившись, что моя примитивная реализация function работает и не требует много памяти, перенес ее в проект, заменив все std::function, до которых смог дотянуться. С чувством выполненного долга я отправился на набережную любоваться закатом.

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;
static auto global_handlers = std::pair<Handler, Handler> {};

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

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

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

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение «cglob»; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec  7)(fl 0x00)(ty   0)(scl   3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec  7)(fl 0x00)(ty   0)(scl   3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

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

Всем спасибо, всем удачи!




Комментарии (81):

  1. Amomum
    /#22894902 / +1

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


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

    • hhba
      /#22903152

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

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

      • Amomum
        /#22903198

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

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


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


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

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

        • hhba
          /#22903242

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


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

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


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

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

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


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

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


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

          • Amomum
            /#22903278

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

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


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

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


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

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


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


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

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


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

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


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

            • hhba
              /#22903334

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


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

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


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

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


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

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

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


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

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


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

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

              • Amomum
                /#22903376

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

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


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

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


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

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

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


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

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

                • hhba
                  /#22906698

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


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

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


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

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


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

                  • Amomum
                    /#22907818

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

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


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

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

                    • hhba
                      /#22911218

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

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

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


                      Да-да-да… Там вон ниже гражданин возмущается, но я воздержусь от ответа, так как в общих чертах его уже знаю — «вам надо было написать программу совершенно иначе, и тогда не было бы проблем». Этим плюсы и «прекрасны» для любого человека на планете, кроме, вероятно, khim-а, что как бы вы ни написали ваш код на плюсах, где-то в последнем стандарте есть способ написать его более правильно.

                      • Amomum
                        /#22911514

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

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

                        • hhba
                          /#22912936

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

                          • Amomum
                            /#22913970

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

                  • holomen
                    /#22908532

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

                    А можно подробнее и про удаленный конструктор копирования и пример одной забытой буквы? Спасибо.

                    • hhba
                      /#22911250

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

                      • holomen
                        /#22918526

                        Да это не дискуссия, действительно интересно что это такое.

  2. staticmain
    /#22895156

    Статья о том, как самим себе организовать проблем а потом героически их решать.

    • dandemidow
      /#22895912

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

      • vau
        /#22900054 / +1

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

  3. DustCn
    /#22895634

    Троллейбус.жпег

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

    Вы любите жить в кредит? Писать ПО в кредит точно у вас хорошо получается…

    • dandemidow
      /#22896016

      Да, жить в кредит совсем грустно, наверное.
      Можно отказаться от исключений, но это не значит, что прошивка никогда не упадет. Но мы ни разу не видели сбоя в рантайме по причине проскочившего шального исключения. Мы соблюдали технику безопасности. Аналогично с умным указателем: разница в размере дополнительной информации образуется из-за разницы в реализации под конкретный компилятор. Эмпирический подбор состоял в том, что прошивка не собиралась, если памяти было мало. Хоть китайское электричество до конца не изучено, но тут от фазы луны мало что зависело. (upd: в настоящее время все shared_ptr удалены)
      Мы никогда не полагаемся на удачу или UB.

  4. tea1975
    /#22895676

    Автор, пиши ещё! Стиль супер!
    Интересно было бы в сравнение добавить Keil.

    • dandemidow
      /#22896064

      Спасибо! Я бы и рад, но пока Keil ускользает от меня.

  5. guinzoo
    /#22895864

    Давно уже есть готовая реализация std::function без аллокаций https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h

    • dandemidow
      /#22896046

      Реализации есть, я видел несколько своими глазами. Но у всех есть фатальный недостаток. В таких проектах бывает очень сложно привлекать сторонние библиотеки. Телодвижений придется исполнить настолько много, что поневоле приходишь к мысли, что уж лучше свой моноцикл.

  6. ionicman
    /#22896168 / -1

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

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

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

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

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

    • lamerok
      /#22896678

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

      • NN1
        /#22896690

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

        • lamerok
          /#22896794 / -1

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

          • Videoman
            /#22896862 / +1

            Всё таки это абсолютно разные вещи:
            sizeof — возвращает размер в char-ах
            std::size — возвращает количество элементов

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

            • lamerok
              /#22896892 / -1

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


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

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

              • Videoman
                /#22896980

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

                • picul
                  /#22897058

                  sizeof(array<int,10>) >= sizeof(int[10])
                  Это классическая перестраховочное допущение. На практике я не вижу причины, почему не должно выполняться равенство. Или может у Вас они есть?

                  • Videoman
                    /#22897196

                    Это не ко мне, а к автору утверждения. Я только ответил на вопрос почему иcпользуется sizeof, а не std::size.

                • lamerok
                  /#22897696 / +1

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


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

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



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

                  • Videoman
                    /#22898546

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

      • ionicman
        /#22896738

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

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

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

        • picul
          /#22897040

          А можно пример проявления оверхеда в std::array<T, N>?

          • NN1
            /#22910762

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


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

            • picul
              /#22911448

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

              • NN1
                /#22911478

                Вот бы было указано в стандарте, что 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
                

                • picul
                  /#22911638

                  В C++ в принципе с массивами нулевой длины есть неудобства. Они вызывают варнинг, который мне всегда приходится отключать прагмой, когда объявляется такой массив. Насколько я понимаю, это из-за того, что в C++ объект не может иметь нулевой размер.
                  На счет size_t — ну так это правильный тип для размера в памяти, надо приведение — просто пару букв дописать. А вот sizeof(a)/sizeof(a[0]) выглядит архаично, так и не скажешь, дружит ли оно с выравниванием, ну и есть это:

                  template<typename T, uptr N>
                  inline constexpr uptr array_size( T ( & )[N] )
                  {
                      return N;
                  }

                  • NN1
                    /#22911662

                    Конечно в коде не будет так, а будет макрос "_countof", который и проверяет тип на массив.
                    Легко сказать, что нужно приведение типа каждый раз, но это банально неудобно.

                • lamerok
                  /#22913104

                  sizeof(a)/sizeof(a[0]

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


                  constexpr auto size_a = a.size()

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

                  • NN1
                    /#22913396

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

                    • lamerok
                      /#22913424

                      Тогда нужно в любом случае явно кастить, иначе у вас чисто теоретически в другом проекте может быть косяк.
                      Дело в том, что 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());

                      • NN1
                        /#22913508

                        Без приведения типа будет предупреждение даже с constexpr: gcc.godbolt.org/z/EeWsGna3E

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

                        • lamerok
                          /#22913780

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


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

                          • NN1
                            /#22913802

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

                            (5): warning C4305: 'initializing': truncation from 'unsigned __int64' to 'short'
                            <source>(5): warning C4309: 'initializing': truncation of constant value
                            </code>
                            
                            Мы ведь не игнорируем предупреждения компилятора, не так ли ? ;)

                            • lamerok
                              /#22913812

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

                • Tujh
                  /#22913838

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

        • lamerok
          /#22897706

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


        • Tujh
          /#22898360

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

        • ncr
          /#22901216 / -1

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

          Да-да — сначала программист умный и всё понимает, а потом внезапно heartbleed.

  7. SignallerK
    /#22897894 / +1

    А с точки зрения всяких DIY проектов, что выходит лучше сидеть на си и не рыпаться?

    • jaiprakash
      /#22898060

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

    • dandemidow
      /#22898362 / -1

      Мне кажется, не стоит себя так ограничивать. Даже с точки зрения DIY

    • holomen
      /#22903698

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

      плюс таки сильнее контроль типов, шаблоны, constexpr, вот это вот все…

      • hhba
        /#22911274

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

        На самом деле ваше сравнение неудачное имхо, принципиальных отличий нет, вызываете ли вы foo(*bar, baz...) или bar.foo(baz...). Как по мне, кратно более крутое отличие, понятное широкой аудитории — наличие неймспейсов в плюсах.

        • holomen
          /#22918548

          Может и неудачное, но меня просто вымораживало когда писал управление несколькими шаговиками вот именно это место, а плюсы низзяя было… %)
          а принципиальное отличие… left[автодополнение].s[автодополнение]([выпадает нужный список параметров, заполняем]); гораздо быстрее.
          В принципе да, встречается такое автодополнение что лучше бы его не было, но ведь всегда можно взять внешний редактор который вот прям как надо.

          • Tujh
            /#22918730

            Справедливости ради, описанное ООП вполне себе штатно реализуется и на чистом си через структуру с указателями на методы, рукописный вариант vtable из плюсов.

  8. YetAnotherSlava
    /#22898078 / +1

    Именно для этого и была придумана Ада.

  9. FrozenWalrus
    /#22898488

    Вместо тяжеловесной std::function зачастую возможно использовать более легковесные non-owning non-allocating аналоги, например llvm::function_ref из проекта LLVM.

  10. NeoCode
    /#22898526 / +1

    Ни разу не потребовалось в embedded программинге ни динамическая аллокация, ни исключения, ни умные указатели. Вся память выделяется статически на этапе компиляции. Структуры данных в основном — циклические буфера фиксированного размера, кратного степени 2. Ну или просто некий глобальный объект, в котором в структурном виде собраны все переменные, описывающие состояние системы.

    • Amomum
      /#22898570

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

  11. devlato
    /#22898946

    Как насчёт написания мелкого аллигатора и использования placement new на статически выделенном массиве?

    • Tujh
      /#22902146

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

      • Amomum
        /#22903546

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

        • Tujh
          /#22904762

          Тогда можно и с аллокатором. не возиться :)

        • hhba
          /#22906746

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

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

          • Amomum
            /#22907828

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

  12. Dark_Purple
    /#22899414

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

  13. lamerok
    /#22908788

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


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

    На самом деле есть, все описано в руководстве на С++, в разделе "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 — работает хорошо, а исключением известных багов, которые хорошо описаны.

    • hhba
      /#22911330

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

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


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

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

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


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

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


      А почему? И это, разве есть способ никогда-никогда не использовать shared_ptr?

      • Tujh
        /#22913870

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

        • hhba
          /#22914136

          Стоп-стоп, ну конечно, если вам не нужно расшаривание, то не нужен и shared_ptr. А если нужно?