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

    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!

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

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

    Auriga
    Аурига — это люди

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

      +2

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


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

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

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

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


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


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

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

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


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

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


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

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

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


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

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


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

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


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

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


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

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


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


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

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


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

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


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

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


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

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


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

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


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

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

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


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

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


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

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

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


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

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


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

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

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


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

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

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


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

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


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

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


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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

                  +1
                  Реализации есть, я видел несколько своими глазами. Но у всех есть фатальный недостаток. В таких проектах бывает очень сложно привлекать сторонние библиотеки. Телодвижений придется исполнить настолько много, что поневоле приходишь к мысли, что уж лучше свой моноцикл.
                    0
                    а ещё уже давно существует www.etlcpp.com/home.html
                    и в частности www.etlcpp.com/function.html
                    всяко лучше своего моноцикла
                      0
                      Не спорю, тут дело скорее во вяких бюрократических кунштюках
                  0
                  Т.е. люди пытаются в ограниченных условиях, вместо использования специально для этого созданных библиотек или подходов, использовать стандартные, предназначенные для абсолютно другого окружения, и после этого героически падать? Причем падать глупо и все время на лицо.

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

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

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

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

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

                      0

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

                        0

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

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

                            0

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


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

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

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

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


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

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



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

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

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

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

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

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

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


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

                                  0
                                  Да, проверил в MSVC — размер std::array<int, 0> равен 4-ем байтам. С другой стороны, я знаю лишь один вариант использования массивов нулевой длины, и в этом случае std::array не подходит в принципе, нужен нативный массив.
                                  В остальном — вообще на много чего нет гарантий. На то что компилятор не вставит Sleep(1000) после каждой инструкции — тоже гарантии нет. Но вообще он не вставляет.
                                    0
                                    Вот бы было указано в стандарте, что 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
                                    
                                      0
                                      В C++ в принципе с массивами нулевой длины есть неудобства. Они вызывают варнинг, который мне всегда приходится отключать прагмой, когда объявляется такой массив. Насколько я понимаю, это из-за того, что в C++ объект не может иметь нулевой размер.
                                      На счет size_t — ну так это правильный тип для размера в памяти, надо приведение — просто пару букв дописать. А вот sizeof(a)/sizeof(a[0]) выглядит архаично, так и не скажешь, дружит ли оно с выравниванием, ну и есть это:
                                      template<typename T, uptr N>
                                      inline constexpr uptr array_size( T ( & )[N] )
                                      {
                                          return N;
                                      }
                                        0
                                        Конечно в коде не будет так, а будет макрос "_countof", который и проверяет тип на массив.
                                        Легко сказать, что нужно приведение типа каждый раз, но это банально неудобно.
                                0

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


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

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

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

                                        На самом деле ваше сравнение неудачное имхо, принципиальных отличий нет, вызываете ли вы foo(*bar, baz...) или bar.foo(baz...). Как по мне, кратно более крутое отличие, понятное широкой аудитории — наличие неймспейсов в плюсах.
                                      +1
                                      Именно для этого и была придумана Ада.
                                        0
                                        Вместо тяжеловесной std::function зачастую возможно использовать более легковесные non-owning non-allocating аналоги, например llvm::function_ref из проекта LLVM.
                                          +1
                                          Ни разу не потребовалось в embedded программинге ни динамическая аллокация, ни исключения, ни умные указатели. Вся память выделяется статически на этапе компиляции. Структуры данных в основном — циклические буфера фиксированного размера, кратного степени 2. Ну или просто некий глобальный объект, в котором в структурном виде собраны все переменные, описывающие состояние системы.
                                            0

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

                                            0

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

                                              0

                                              *аллокатора

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

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

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

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

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

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

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


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

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

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

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


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

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

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


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

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


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

                                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                    Самое читаемое