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

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

А Вы пробовали в C++ вообще не использовать явное выделение памяти в куче? Понятно, что использование контейнеров и умных указателей будет кушать больше памяти. Просто интересно, насколько это вообще прогодно для микроконтроллеров. Хотя кажется, что там где есть 512МБ ОЗУ, уже можно практически не беспокоутся…
Я, собственно, так и делаю. Динамическое выделение памяти не нужно. Практически всегда можно оценить верхний предел используемой памяти и выделить его статически или на стеке. На крайняк есть alloca. Ну, или можно свой менеджер памяти написать, без фрагментации.

Да, почти все стандартные контейнеры нельзя использовать — ну и ладно. Зато есть шаблоны, RAII, интерфейсы, std::fill вместо memset'a и std::copy вместо memcpy.

Если бы Кейл полноценно поддерживал С+11 (а он уже почти, только компиляция на лету все еще ругается), то были бы еще весьма удобные вещи вроде std::array, auto и std::function. Но многое можно и самому написать в упрощенном виде или в Бусте взять.

Из минусов:

  • время компиляции заметно больше по сравнению с С
  • go to definition в кейле не различает методы с одинаковыми именами в разных классах
  • на шаблонный код иногда не поставить брейкпоинт и даже не прошагать, но именно что иногда
  • мутные приколы кейла с виртуальными деструкторами

В основном это минусы Кейла как среды разработки, а не С++.
Динамическое выделение памяти не нужно

Соглашусь с Вами — для большого класса задач (даже вне области embedded) сущности определены на начало работы системы, и их динамическое порождение/уничтожение излишне.

В нашем проекте все места, где допустимо использование динамической памяти, уже давно известны. Как правило, парные malloc/free при этом содержатся в пределах одной функции. Любой код-ревью, содержащий malloc, вызывает тяжёлый взгляд и долгое обсуждение. А если к нему не идёт free...
Я его и не выделяю. У меня все классы либо статические сами по себе либо статически объявлены в стеке. Для микроконтроллера и 2 МБ ОЗУ это уже роскошь на мой взгляд. Хотя все зависит от задачи.
То, что в коде на Си вполне можно использовать ООП — это довольно старый, но почему-то малоизвестный широкой публике факт. Поглядите на ядро Linux — написано на Си, при этом имеет развитые иерархии классов драйверов, абстрактные классы для файловых систем и т.п., есть в наличии аналог dynamic_cast (более опасный)...

С++ не привнёс в идеи ООП существенного. Да и исполнение принципов ООП в нём, надо сказать, слабенькое — что угодно кастуется к чему угодно, вся память на ладони.

Что, по-моему, действительно является шагами вперёд по сравнению с Си, — это следующие три вещи.

  1. Пространства имён (namespace). В Си приходится придумывать всякие префиксы к функциям, структурам, типам и т.п. В мелких проектах это несложно, но с ростом масштабов это становится тормозом.

  2. Механизм исключений (exceptions). В embedded-проектах его, как правило, не задействуют (как раз из-за требований по памяти, времени и стеку); этому помогает его необязательность в C++. Но в традиционном программировании возможность разделить обработку ошибок от главной ветки кода очень ускоряет работу.

  3. Шаблоны (templates). Замена костыльным макросам Си. При отладке разбирать макросы — сущее мучение. С шаблонами при отладке чуть полегче. Хотя любой, кто хоть раз пытался разобраться в выводе ошибки компилятора для шаблонизированного кода (пресловутый template vomit) со мной не согласится :-)
Мы в одном проекте попробовали писать объектно на С, но очень быстро устали. Слишком многословно получается. Да и какой смысл писать руками то, что компилятор может делать за тебя?
Да, С++ более запутанный, но все его фишки никто не заставляет применять. Можно спокойно жить в "С с классами".

А уж на GObject'ы мне лично смотреть страшно.
Уже давно использую "C++ с ограничениями" для микроконтроллеров, так как читабельнсть и структурирование кода в разы выше, чем на С. Если проект большой и сложный, то C++ здорово выручает. При этом практически не отличается от C в плане потребления ресурсов. Вот ограничения:

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

  2. не используются конструкторы и деструкторы, кроме пустых конструкторов прямой инициализации:
    SomeClass(int v1, int v2): member1(v1), member2(v2) {}

    • они компилятором правильно разворачиваются без доп. кода.

  3. Если предполагается создать всего один экземпляр класса, то все функции и члены класса — статические. Результат компиляции ничем не отличается от С, но выигрывает синтаксисом.

  4. Использование интерфейсов и мультинаследования бывает очень удобно, но приводит к расходу памяти (все нужные объекты всех реализаций создаются статически, и потом просто выбирается необходимая реализация). Впрочем, тут можно очень аккуратно использовать malloc — как правило, выбор реализаций интерфейса зависит от настроек и создается один раз при старте системы, удалять его не нужно и дефрагментация не страшна.
не используются конструкторы и деструкторы, кроме пустых конструкторов прямой инициализации:
SomeClass(int v1, int v2): member1(v1), member2(v2) {}

А почему? Ведь можно сделать, например, критическую секцию через RAII, из которой невозможно забыть выйти.
Причина в ограниченных ресурсах памяти FLASH и производительности. Конструкторы тянут достаточно много сервисного кода за собой, зато при их отсутсвии код сравним с генерируемым из С.
Не могли бы вы уточнить, что имеется в виду под сервисным кодом? В самом общем случае конструкторы — это всего лишь функции, которые вызываются при создании объекта. Когда я в своё время рассматривал, какие из возможностей C++ могут подложить свинью во встраиваемом ПО, конструкторы на особом подозрении у меня не были. Понятно, что так получаются неявные вызовы в коде, но это уже другая история.
Да я особо не разбирался, просто достаточно упомянуть оператор 'new' и размер бинарника увеличивается на 40к. Это сам механизм конструкторов и выделения памяти, даже если сами фукнции пустые.
Думаю, стоит копнуть поглубже — возможно, рост бинарника вызван другими причинами, и вы себя напрасно ограничиваете. Конструкторы ведь никак не зависят от того, динамически создаются объекты или статически — и тут и там код конструкторов один и тот же. Скорее всего, когда вы упоминаете в проекте new, то однократно тянется фрагмент рантайма, связанный с динамическим выделением памяти или, например, с исключениями (если разрешены в проекте).

Если вы используете GCC и Newlib, там есть грабли с резким ростом размера кода при использовании абстрактных классов, и ещё одни грабли — если включены и используются исключения (а оператор new бросает исключение при нехватке памяти). Плюс, конечно, убедиться в том, что компилятору и компоновщику дана отмашка удалять неиспользуемый код.
В общем то new мне не нужен в проекте, поэтому я глубже не копал. Но думаю, вытянуть только часть функционала C++ связанного с конструкторами, и не тянуть исключения и прочее, будет достаточно сложно. Кстати, при создании объекта статически конструкторы не отрабатывают. Тоже нужно разибираться, почему.

Удаление неиспользуемых функций включено.
Это, признаться, ужасно странно — с точки зрения языка что вы пишете "MyClass my_class;", что "MyClass* my_class = new MyClass;", должен вызваться один и тот же конструктор. Если для локальных переменных конструктор вызывается а для глобальных — нет, можно посмотреть, вызывает ли стартовый код __libc_init_array() перед прыжком в main().

Если у вас GCC и исключения вам не нужны, можете их просто запретить (-fno-exceptions), компоновщик тогда не должен потянуть связанный с ними код. Если нужны, попробуйте определить собственную __gnu_cxx::__verbose_terminate_handler(), стандартная из Newlib чудовищно раздута, а всё что ей надо делать — реагировать на непойманное исключение. В нагрузку, если используете абстрактные базовые классы, сделайте свою extern "C" void __cxa_pure_virtual(), с ней та же история. Из стандартной библиотеки торчит довольно много ручек, которые можно покрутить и подёргать.
Конструкторы к new не имеют никакого отношения. И естественно не увеличивают память и т.п. Это можно очень просто проверить, создав объект с конструктором в виде локальной переменной.
Да, но они и не рабатают как надо. Выше я написал, что компилятор из всего конструктора выполняет только инициализацию переменных, которые указаны до фигурных скобок. Само тело конструктора не выполняется.
Если не работают, то это может означать только то баг компилятора. Кстати, о каком идёт речь?

У меня например всё без проблем работает.
Если переменные-объекты, для которых не вызываются конструкторы объявлены как глобальные или статические внутри классов или функций, и вы работаете с тулчейном arm-none-eabi-gcc, то вполне возможно вы используете ld-скрипт и startup-код, который не поддерживает вызов конструкторов глобальных переменных. Если вкратце — компилятор кладет список адресов конструкторов в секции .preinit_array и .init_array, поэтому чтобы оно заработало нужно:
1) добавить в ld-скрипт соответствующие секции
.init_array: {
. = ALIGN(4);
init_array_begin = .;
KEEP ((.init_array))
init_array_end = .;
} >rom
2) сделать доступными указатели на начало и конец секции в коде, например так:
typedef void (func_t)();
extern func_t
init_array_begin;
extern func_t
init_array_end;
3) в startup-коде (обычно это reset-handler) перед вызовом функции main но после обнуления секции .bss и загрузки секции .data добавить что-то вроде:
for( func_t f = &init_array_begin; f != &__init_array_end; ++f )
(
f)();


Разумеется то же самое нужно сделать для секции .preinit_array, и если вы хотите, чтобы вызывались деструкторы, то и для .fini_array, только деструкторы вызывать после main. Это если вкратце. А если подробно, то вдумчиво читать документацию про то, как пишутся ld-скрипты, и изучать примеры, например в libopencm3 вся эта инициализация есть, исходники можно на гитхабе посмотреть.

Дико извиняюсь за предыдущий комментарий, забыл про тег source, а исправить это месиво не успел (

Если переменные-объекты, для которых не вызываются конструкторы объявлены как глобальные или статические внутри классов или функций, и вы работаете с тулчейном arm-none-eabi-gcc, то вполне возможно вы используете ld-скрипт и startup-код, который не поддерживает вызов конструкторов глобальных переменных. Если вкратце — компилятор кладет список адресов конструкторов в секции .preinit_array и .init_array, поэтому чтобы оно заработало нужно:
1) добавить в ld-скрипт соответствующие секции

    .init_array: {
        . = ALIGN(4);
        __init_array_begin = .;
        KEEP ((.init_array))
        __init_array_end = .;
    } >rom

2) сделать доступными указатели на начало и конец секции в коде, например так:

    typedef void (func_t)();
    extern func_t __init_array_begin;
    extern func_t __init_array_end;

3) в startup-коде (обычно это reset-handler) перед вызовом функции main но после обнуления секции .bss и загрузки секции .data добавить что-то вроде:

for( func_t * f = &__init_array_begin; f != &__init_array_end; ++f )
    (*f)();

Разумеется то же самое нужно сделать для секции .preinit_array, и если вы хотите, чтобы вызывались деструкторы, то и для .fini_array, только деструкторы вызывать после main. Это если вкратце. А если подробно, то вдумчиво читать документацию про то, как пишутся ld-скрипты, и изучать примеры, например в libopencm3 вся эта инициализация есть, исходники можно на гитхабе посмотреть.
Спасибо за развернутое объяснение, попробую на практике. ld-скрипты это то что я пропустил из-за их сложности, остановился на первом рабочем примере из того, что удалось найти.
Статья произвела двойственное впечатление — тема поднята интересная, но совершенно не раскрыта, вместо серьезного обсуждения автор начал объяснять нам механизм фрагментации памяти — тот, кто знал, пропустил, кто не знал — все равно не понял.
Позволю себе несколько дополнений.

Первое — конструкторы и деструкторы именно нужны (особенно конструкторы), независимо от присутствия динамических объектов, поскольку, на мой взгляд, наличие конструкторов есть одно из основных преимуществ в С+ перед С, поскольку не позволяет Вам забыть об инициализации используемого объекта (например, UARTа).

Второе — очень важна инкапсуляция, которая не позволит Вам обращаться к объекту несанкционированными способами (точнее, не позволит легко и непринужденно обращаться, как в С), что в сочетании с развитым аппаратом enum ставит дополнительную преграду ошибкам (у меня был пост на эту тему).

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

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

Так что мой личный вывод — несомненно использовать, плюсы однозначно минусы перевешивают, в конце концов методику "на С++ как на С никто не запрещал", начните с нее а потом распробуете и потихоньку перейдете на полный С++.
mbed же на С++. И еще может быть критичным производительность. Для mbed актуально.

А конструкторы кстати вполне в данном случае оправданы и для статических классов. Что бы не забыть инициализацию вызвать и для инкапсуляции опять же.
А что вы называете mbed?
На github проекта mbed я увидел несколько десятков проектов.
Где-то половина на С-и.
Самый сложный из кусков mbed — TLS, написан на C-и однако.
>>C-и
Это даже круче чем 2-а и 3-и
А не могли бы вы подробней описать работу в связке Visaul Studio + Keil, было бы интересно почитать.

Ах да, вы не смотрели в сторону SW4STM32? Она тоже поддерживает C++
Я планирую написать отдельную статью про то как использую Visual Studio для тестирования кода. А по поводу IDE — это все на любителя. Мне больше нравится Keil mVision, хотя я пробовал и Eclipse и IAR.
Keil использует ARM Compiler 5, который поддерживет только подмножество C++11. Вряд ли ситуация улучшится в будущем.
Если нужен C++11 и C++14, то ARM рекомендует использовать ARM Compiler 6, который основан на LLVM/Clang последних версий.
Вот например статья: How C++11/14 can improve readability without affecting performance

Главные проблемы с C++ — это размер исполняемого кода и С++ библиотека, слишком много чего тянет за собой C++. В ARM об этом знают и работают над этим. Правда все зависит от того, насколько C++ востребован у кастомеров.
Используем обычный gcc. Код на C++14 (с полиморфными лямбдами и т.п.) спокойно компилируется и работает на МК с флешем в 16КБ и оперативкой в 4КБ. Не пойму откуда у людей сложности.
А как вы избавились от зависимостей от C++ runtime библиотек?
Ни от чего не избавлялись. А зачем? Всё равно же линкуется только непосредственно используемое...

А вот исключения, rtti и threadsafe действительно отключены в настройках компилятора.
Может быть помешал недописанный код первого примера на c++, но я так и не понял:
зачем все навороты с виртуальными функциями («подготовка к полиморфизму»), если дальше всё опять сводится к ifdef'ам?
Вы можете выбрать точку разветвления и поднять ее вплоть до main без особых усилий. Когда у вас маленький проект — это на так важно. А когда огромный — построение связей между частями программы начинает играть осень большую роль.
1) Не могли бы вы поподробнее рассказать (может быть в виде статьи) «о распределенной IoT системе, состоящей из сотен устройств»? Потому что управление освещением и/или гаражными воротами уже не интересно.

2) Visual Studio отличная среда разработки. Но как я понял, вы используете LPC11C24, для которого изготовитель предлагает и даже настойчиво рекомендует Eclipse. Прокомментируете ваш выбор?

3) Сейчас очень много микроконтроллеров на ядре Cortex-M. Почему вы выбрали (или ваш заказчик) LPC11C24, а не STM или Milandr 1986, особенно в свете модного импортозамещения?
1) Смотрите тут.
2) Я не работаю напрямую с LPC11C24 в Visual Studio. Только тестирую там код. Отлаживаюсь я в Keil mVision — мне он нравится больше всего. Я пробовал и другие IDE — не пошли :) На мой взгляд выбор IDE — это как выбирать машину, кому что нравится.
3) LPC11C24 — это CortexM0. Его взяли потому что он во первых маленький (занимает мало места на плате) а во вторых имеет встроенный CAN. На выбор микроконтроллера импортозамещение никак не влияет. Их еще долго будут импортозамещать :)
Забавно. Статья про то, что надо использовать C++ на микроконтроллерах (в принципе правильный тезис) от того, кто похоже по сути не умеет программировать на современном C++ (делает это в стиле Java/C#). Показать в качестве аналога сишного ifdef динамический полиморфизм — это же просто жесть. И это при том, что в C++ имеется в наличие один из лучших среди всех языков механизмом статического полиморфизма. Конечно на фоне такого можно рассуждать о перерасходе памяти в C++ в сравнение с C. Хотя на практике при нормальном использование как раз C++ код может быть оптимальнее за счёт использования множества инструментов времени компиляции (включая метапрограммирование).
А что вы понимаете под оптимальностью?
В данном случае подразумевалась оптимальность кода (минимизация расхода памяти и количества тиков процессора). С этим у C и C++ в большинстве случаев абсолютно одинаково (хорошо, в отличие от того же C#). Ещё бывает оптимальность работы программиста, которая зависит качества абстракций языка. С этим у C всё плохо, а у C++ и C# хорошо. В этом и есть весь смысл C++ — наличие высокоуровневых абстракции без малейшей потери эффективности кода. Ценой же за это является сложность языках.
А разве шалоны не ведут к увеличению объема кода? К примеру у меня есть функция на 10 экранов, которая оперирует матрицами любого размера. Для возможности обращения вида a[i][j] нужно заранее определеить во всех переменных размерность матрицы, и это красиво делается при помощи шаблона. Но когда я вызову эту функцию для матрицы 3X3 и 4x4, разве компилятор не сделает 2 коппии кода фунции? Если задача экономить FLASH память, то очевидно шаблоны не лучшее решение.
Это уже вопрос разумности использования, шаблоны — это просто один из инструментов. В вашем примере с матрицами и большой функцией вероятно да, шаблоны могут привести к раздуванию кода. С другой стороны, в статье ссылку на которую я кинул приводятся примеры использования шаблонов для GPIO вместе с ассемблерными листингами того, что получается на выходе, и получается очень компактно.
К примеру у меня есть функция на 10 экранов, которая оперирует матрицами любого размера.
Так размер матриц задаётся на стадии компиляции или на стадии исполнения. Если на стадии компиляции, то где он задаётся? Если на стадии исполнения, то как передаётся в функцию?
Для возможности обращения вида a[i][j] нужно заранее определеить во всех переменных размерность матрицы, и это красиво делается при помощи шаблона.
А это вообще непонятно. В принципе в C++ для возможности обращений вида a[i][j] просто переопределяют соответствующий оператор. Причём тут размеры или вообще шаблоны неясно. )
Динамическое выделение памяти можно также использовать для создания объектов при инициализации в соответствии с конфигурацией — в этом случае память не нужно освобождать, и фрагментация не возникает. Также использую автоматические временные объекты в стеке. Использую модифицированные шаблоны STL, не использующие динамическую память. Использую свой HAL в виде шаблонов для работы с периферией STM32. Иногда приходится использовать временные объекты, созданные в динамической памяти — в рамках одной функции.
«Использую модифицированные шаблоны STL, не использующие динамическую память. Использую свой HAL в виде шаблонов для работы с периферией STM32. „
эти наработки публично доступны? можно глянуть?
Нет, эти наработки принадлежат компании. Но подобный HAL для AVR и STM32 есть в публичном доступе, и подобные модификации STL — по-моему тоже.
также очень интерестно взглянуть на модифицированный HAL и его возможность связи с CubeMX
Могу посоветовать взглянуть на эти библиотеки:
github.com/andysworkshop/stm32plus
github.com/JorgeAparicio/libstm32pp
github.com/RickKimball/fabooh
github.com/pfalcon/PeripheralTemplateLibrary
Особенно впечатляюще выглядит первая, хоть она и является (являлась) обёрткой над STM32 Standard Peripherals Library
Еще надо упомянуть про некоторую проблему с "красивым" встраиванием обработчиков прерываний в проект на С++. Для микроконтроллера прерывание — это переход на выполнение кода с определенного адреса, сохранив перед этим контекст. Что нативно делается вызовом Си-функции. На Си++ метод класса — это не просто функция. При вызове метода класса метод должен знать, от какого именно он класса, т.е. помимо адреса функции должен передаваться адрес класса (вернее адрес+смещение метода, или как там это внутри у компиляторов делается). Поэтому обработчик прерывания нельзя вот так просто повесить на метод какого-то класса, скажем, метод драйвера SPI ЦАПа. Придется либо пользоваться "нестандартными" расширениями компилятора, который после использования специальных директив позволит так делать для статических классов, либо делать Сишную функцию обработки прерывания, засовывать внутрь файла с реализацией класса, прописывать области видимости, extern'ы и прочий уменьшающий красивость код.

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

Есть вопросы с подсветкой кода. Не все среды разработки под МК уверенно парсят Си++ код и позволяют на ходу подсвечивать ошибки, подсказывать члены класса, переходить по объявлению переменной и т.п. На Си обычно работает у всех без вопросов.

Однако есть определенные плюсы в применении Си++ компилятора даже ведя проект "на Си", без использования классов. Например, Си может позволить вызвать функцию и передать в неё не то число аргументов, может позволить присвоить указатели на функции опять же не глядя на кол-во аргументов, может не напомнить о возвращаемом функцией значении и т.п. По опыту работы с микроконтроллерами Texas Intruments ядра C28 Компилятор Си++ оказался более интересен в плане нахождения ошибок и нестыковок в коде.
Поэтому обработчик прерывания нельзя вот так просто повесить на метод какого-то класса

Хы, ну это же совсем древняя проблема, с кучей давно известных решений. Ведь в том же программирование скажем под Windows тоже нельзя передать функциональный объект в качестве оконной функции и т.п. Однако никто не страдает от этого при написание GUI библиотек под Windows. )))

Есть вопросы с подсветкой кода. Не все среды разработки под МК уверенно парсят Си++ код и позволяют на ходу подсвечивать ошибки, подсказывать члены класса, переходить по объявлению переменной и т.п. На Си обычно работает у всех без вопросов.

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

Решений куча, не спорю, но красота кода по сравнению с Си чуть портится.

Не пойму даже как можно пользоваться этими блокнотами-переростками.

Стараюсь, конечно, пользоваться эклипс для повседневной работы, но иногда нужно бывает что-то перенести в старый проект на другой IDE, бывает, что заказчики, покупающие софт в исходных кодах, говорят "а мы хотим вот в этой IDE, у нас она лицензионная и все привыкли". Ну это всё лирика, конечно.
Я использую C++ в МК возможности только для изоляции кода. Т.е. классы у меня есть, но все методы и переменные у него static. По сути это C но с изоляцией. Все остальное я делать не хочу, т.к. память это весьма критичный ресурс и я должен его контролировать очень жестко. + к тому, ООП это не просто классы, это виртуальные методы. А это уже не только память под VMT (хотя и статическая) но и быстродействие. Причем не контролируемое (точнее менее контролируемое). Поэтому я просто не даю себе возможности потерять контроль над процессом исполнения программы.
З.Ы. Все сказанное исключительно ИМХО
Ну и про более строгую типизацию не надо забывать. Это тоже благо :)
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации