Поддержка C++ на avr в gcc

    Компилятор avrgcc поддерживает C++, однако в его поставку не входит ни стандартная библиотека, ни реализация ABI: служебных функций, вызовы которых вставляет сам компилятор. В результате люди пытаются реализовать те части, которые им нужны, самостоятельно и зачастую делают это не очень хорошо. Например, часто предлагается отстрелить себе ногу определив пустую функцию __cxa_pure_virtual(void) {} или подложить себе грабли, написав заглушки для __cxa_guard_acquire, __cxa_guard_release и __cxa_guard_abort. В данной статье я предлагаю разобраться, чего не хватает для счастья, где это взять или как написать.
    Я знаю, что немло людей считает, что C++ на микроконтроллере не нужен. Их я прошу прочитать последний раздел статьи перед тем, как писать комментарии.

    Особенности для владельцев arduino

    Arduino предоставляет ограниченную поддержку C++. Но, насколько я понимаю, разработчики arduino не любят C++, поэтому в соответсвующий модуль, по просьбам трудящихся, был вставлен первый попавшийся костыль. Им оказался костыль, описанный на avrfreaks, причем без исправлений, указанных в комментариях к топику. Поэтому сперва вам придется от него избавиться. Удалите файлы
    • hardware/arduino/cores/arduino/new.h
    • hardware/arduino/cores/arduino/new.cpp
    Или используйте версию, где это уже сделано.

    Чисто виртуальные и удаленные методы

    Механизм виртуальных методов, как правило, реализуется через vtable. Это не регламентировано стандартом, однако используется во всех компиляторах. Даже если вы объявляете метод чисто виртуальным, то есть не имеющим реализации, в vtable все равно будет отведено место под указатель на этот метод. Это необходимо для того, чтобы у дочерних классов по этому же смещению положить указатель на соответсвующую реализацию. Вместо указателя на отсутствющий метод компилятор записывает в vtable указатель на функцию-заглушку __cxa_pure_virtual. Если кто-то сумеет вызвать чисто виртуальную функцию, то управление перейдет на заглушку и она остановит программу, вместо того, чтобы пытаться исполнить случайный кусок памяти. Обратите внимание, что эта защита обходится почти бесплатно: реализация __cxa_pure_virtual, содержащая единственный вызов, занимает всего 6 байт флеша.
    Возникает резонный вопрос, а как вообще можно вызвать чисто виртуальную функцию, если нельзя создать объект абстрактного класса? Создать нельзя, а вызвать можно, если писать странный код:
    class B {
    public:
        B() {
            // Объект класса C пока не создан, поэтому используется vtable класса B
            // Однако в конструкторе не работает механизм виртуальных функций, поэтому
            // мы не можем вызвать не сущесутвющую функцию virt(), но можем вызвать
            non_virtual();
        }
    
        void non_virtual() {
            // Это обычная функция и она не знает, что вызвана из конструктора
            // поэтому она выполнит вызов виртуальной функции, причем вызовет
            // реализацию для класса B
            virt();
            // pure virtual method called
            // terminate called without an active exception
            // Аварийный останов (core dumped)
        }
    
        virtual void virt() = 0;
    };
    
    class C : public B{
    public:
        virtual void virt() {}
    };
    
    int main(int argc, char** argv) {
        C c;
        return 0;
    }
    
    Чтобы не возникало таких ошибок, надо стараться не вызывать методы объекта, пока он не инициализирован. Другими словами, не надо делать сложную работу в конструкторе. А чтобы компилятор собрал ваше приложение, добавте реализацию следующих функций:
    void __cxa_pure_virtual(void) {
        // We might want to write some diagnostics to uart in this case
        std::terminate();
    }
    
    void __cxa_deleted_virtual(void) {
        // We might want to write some diagnostics to uart in this case
        std::terminate();
    }
    
    Также вам понадобится реализация std::terminate из стандартной библиотеки. Если очень нужно экономить 2 байта оперативной памяти и 14 байт флеша, можно вызывать и abort() напрямую, однако позже я расскажу, почему std::terminate предпочтительнее.

    Статические переменные

    Вы можете объявить переменную внутри функции статической, что фактически сделает ее глобальной переменной видимой только внутри функции.
    int counter(int start) {
        static int cnt = start;
        return ++cnt;
    }
    
    В C этот код не соберется, так как статическая переменная должна инициализироваться константой, чтобы сразу поместить начальное значение в секцию .data. В C++ это было разрешено, однако не специфицировалось, что произойдет в случае, если два потока попытаются инициализировать переменную одновременно. Многие компиляторы предпочли добавить блокировки и обеспечить потокобезопасность в этом случае, а в C++11 такое поведение стало частью стандарта. Поэтому инициализацию не константным значением статической переменной gcc развернет в следующий код: gcc/cp/decl.c
    static <type> guard;
    if (!guard.first_byte) {
        if (__cxa_guard_acquire (&guard)) {
            bool flag = false;
            try {
                // Do initialization.
                flag = true; __cxa_guard_release (&guard);
                // Register variable for destruction at end of program.
            } catch {
                if (!flag) __cxa_guard_abort (&guard);
            }
        }
    }
    
    где guard — целочисленный тип, достаточного размера чтобы хранить флаг и мьютекс. Просмотр исходников gcc показал, что его оптимизацией заморачивались только на ARM архитектуре:
    gcc/config/arm/arm.c
    /* The generic C++ ABI says 64-bit (long long).  The EABI says 32-bit.  */
    static tree
    arm_cxx_guard_type (void)
    {
      return TARGET_AAPCS_BASED ? integer_type_node : long_long_integer_type_node;
    }
    
    Во всех остальных случаях используется тип по-умолчанию: long_long_integer_type_node. На avr, в зависимости от опции -mint8 он будет либо 64, либо 32 бита. Нам хватит и 16. guard.first_byte, в котором размещается флаг, понимается компилятором как байт с наименьшим адресом: *(reinterpret_cast<char*>(g)). Исключением является платформа ARM, где используется только один бит первого байта.

    Как правильно?

    Если вам не нужны потокобезопасные статические переменные, то отключите их опцией -fno-threadsafe-statics и компилятор вместо сложных блокировок поставит простую проверку флага. Реализовывать __cxa_guard_* в этом случае не нужно. Но если вы их предоставляете (как это сделано в arduino), то реализация должна обеспечивать корректную работу в случае одновременной инициализации переменной из обычного кода и из прерывания. Другими словами, __cxa_guard_acquire должна блокировать прерывания, а __cxa_guard_release и __cxa_guard_abort должны их возвращать к предыдущему состоянию. В случае использования RTOS я, возможно, готов пожертвовать корректностью в прерываниях, оставив корректность для двух потоков.Корректная реализация должна работать вот так:
    namespace {
    // guard is an integer type big enough to hold flag and a mutex. 
    // By default gcc uses long long int and avr ABI does not change it
    // So we have 32 or 64 bits available. Actually, we need 16.
    
    inline char& flag_part(__guard *g) {
        return *(reinterpret_cast<char*>(g));
    }
    
    inline uint8_t& sreg_part(__guard *g) {
        return *(reinterpret_cast<uint8_t*>(g) + sizeof(char));
    }
    }
    
    int __cxa_guard_acquire(__guard *g) {
        uint8_t oldSREG = SREG;
        cli();
        // Initialization of static variable has to be done with blocked interrupts
        // because if this function is called from interrupt and sees that somebody
        // else is already doing initialization it MUST wait until initializations
        // is complete. That's impossible.
        // If you don't want this overhead compile with -fno-threadsafe-statics
        if (flag_part(g)) {
            SREG = oldSREG;
            return false;
        } else {
            sreg_part(g) = oldSREG;
            return true;
        }
    }
    
    void __cxa_guard_release (__guard *g) {
        flag_part(g) = 1;
        SREG = sreg_part(g);
    }
    
    void __cxa_guard_abort (__guard *g) {
        SREG = sreg_part(g);
    }
    


    Сколько стоит

    Если вы не используете статические переменные или присваиваете им константные значения — то бесплатно. Если вы указываете флаг -fno-threadsafe-statics, то платите 8 байт оперативной памяти за флаг, и 12 байт флеша на каждую переменную. Если же вы используете потокобезопасную инициализацию, потратите еще 38 байт флеша на каждую переменную и еще 44 на всю программу. Кроме того, на время инициализации статических переменных будут заблокированы прерывания. Но вы же не делаете сложную работу в конструкторах?
    Выбор за вами, но в любом случае, если библиотека предоставляет функции __cxa_guard_*, они должны быть реализованы корректно, а не являться той затычкой, которую везде предлагают. А вообще, я рекомендовал бы стараться не использовать статические переменные.

    Где взять

    abi.h и abi.cpp

    operator new и operator delete

    Когда речь заходит об операторах new и delete обязательно кто-нибудь скажет, что в микроконтроллерах крайне мало памяти, поэтому динамическая память — непозволительная роскошь. Эти люди не знают, что new и delete — это не только управление динамической памятью. Есть еще placement new, располагающий объект в выделенном программистом буфере. Без него не напишешь любимый разработчиками встроенного ПО кольцевой буфер, через который реализуются очереди сообщений. Ну и если вы так уверены в том, что динамическая память не нужна, то зачем написали реализации для malloc и free? Значит есть задачи, где без них обойтись не получилось.

    Типы операторов new и delete

    Во-первых, есть operator new выделяющий память под одиночные объекты и есть operator new[], выделяющий память под массивы. Технически они отличаются тем, что new[] запоминает размер массива, чтобы при удалении вызвать деструктор у каждого элемента. Поэтому важно при освобождении памяти использовать парный operator delete или operator delete[].
    Во-вторых, каждый из этих операторов, как и любая функция в C++, может быть перегружен. И стандартом определены три варианта:
    1. void* operator new(std::size_t numBytes) throw(std::bad_alloc);
      выделит блок памяти размером numBytes. В случае ошибки кидает исключение std::bad_alloc
    2. void* operator new(std::size_t numBytes, const std::nothrow_t& ) throw();
      выделит блок памяти размером numBytes. В случае ошибки возвращает nullptr
    3. inline void* operator new(std::size_t, void* ptr) throw() {return ptr; }
      placement new, располагает объект там, где сказали. Используется при реализации контейнеров
    В arduino, по непонятным причинам, реализован только void* operator new(std::size_t numBytes) throw(std::bad_alloc), причем в случае ошибки он возвращает 0, что приводит к неопределенному поведению программы, так как возвращаемое значение никто не проверяет.
    С operator delete все немного хитрее. Есть void* operator delete(std::size_t numBytes) и void* operator delete[](std::size_t numBytes). Вы можете его перегрузить для других параметров, но не можете вызвать эти перегрузки, так как в языке нет соответсвующего синтаксиса. Есть только один случай, когда компилятор вызовет перегруженные версии оператора delete. Представьте, что вы создаете объект в динамической памяти, оператор new успешно выделил память, ее начал заполнять конструктор и кинул исключение. Ваш код еще не получил в свое распоряжение указатель на объект, так что он не может вернуть системе память объекта-неудачника. Поэтому компилятор вынужден сам это сделать вызвав delete. Но что произойдет, если память «была выделена» с помощью placement new? В этом случае нельзя вызывать обычный delete, поэтому, если конструктор бросил исключение, компилятор вызовет перегруженную версию delete с теми же параметрами, с которыми был вызван new. Так что в стандартной библиотеке определено три версии operator delete и три версии operator delete[].

    Обработка bad_alloc

    Как было сказано выше, наиболее часто используемая версия new обязана кидать исключение, в случае ошибки. Но gcc не поддерживает исключения на avr: их нельзя ни кинуть, ни поймать. Но если их нельзя ловить, то в программе нет ни одной try секции, а значит, если бы исключение было кинуто, то вызвался бы std::terminate. Более того, стандарт C++ позволяет в таком случае (см. 15.5.1) не разматывать стек. Поэтому new может вызвать std::terminate напрямую и это будет соответствовать стандарту.
    Не надо ужасаться, что стандартная библиотека возьмет и завершит прошивку! Часто ли вы можете что-то исправить, если произошел bad_alloc? Как правило, ничего. Ваша прошивка не может продолжать корректную работу и слава богу, что она завершится в момент возникновения ошибки. Но если вы знаете, как поправить ситуацию, вы можете воспользоваться nothrow версией оператора new. Посмотрите на это как на безопасный malloc, корректно себя ведущий в случае, если вы не проверяете возвращаемое им значение.

    Где взять

    В uClibc++ есть полная и корректная реализация new и delete. Правда вместо std::terminate там вызывается abort(). Поэтому я сделал исправленную версию. Заодно туда добавлены списки инициализации, std::move и std::forvard.

    std::terminate vs abort()

    Согласно документации на avr-libc, функция abort() блокирует все прерывания, после чего впадает в бесконечный цикл. Это не то, чего бы мне хотелось. По двум причинам. Во-первых, она оставляет устройство в опасном состоянии. Представьте, что система управляет нагревательным элементом и программа зациклится в момент, когда он включен. В случае ошибки я хочу перейти в безопасное состояние, установив все выходы платы в 0. Во-вторых, я уже знаю, что все плохо и мне не нужно дожидаться, пока сработает watchdog и перезагрузит систему. Это можно сделать немедленно.
    Если прошивка завершается по std::terminate, я могу установить собственный обработчик и выполнить там все необходимые действия. Переопределить abort я не могу: механизм, предусмотренный для этого в unix не работает на avr. Поэтому я лучше потрачу те 2 байта оперативной памяти и 14 байт флеша, которые занимает реализация std::terminate.

    Исключения

    Исключения — та часть C++ за которую приходится платить много, причем не только в коде, который их использует напрямую, но и в коде, через который может пролететь исключение: компилятор вынужден регистрировать деструктор каждой переменной, создаваемой на стеке. Кроме того, для исключений нужен RTTI и небольшой резервный буфер памяти, чтобы было, где создать std::bad_alloc, когда память кончится. Кроме того, это единственная часть C++, для которой проблематично, хотя и не невозможно, рассчитать время выполнения. Насколько я понимаю, у любого, кто достаточно разбирался, чтобы написать недостающие на AVR функции поддержки работы исключений, пропадало желание это делать. Находится много более важных вещей, которые тоже надо сделать. Поэтому поддержки исключений на AVR в gcc нет и, вполне вероятно, что и не будет.

    STL

    Я видел много сообщений, что STL на микроконтроллере — это плохо: она раздувает код и, делая сложные вещи простыми, подстрекает ими пользоваться. При этом умалчивается, что в STL есть и такие примитивы, как быстрая сортировка, которая значительно быстрее и компактнее qsort, или бинарный поиск, безопасные версии min и max. Вы правда знаете сакральный способ написать классические алгоритмы эффективнее других программистов? А тогда почему бы не использовать готовый оттестированный алгоритм, который займет столько же места, сколько то, что вам придется написать. За те части STL, которые вы не используете, вы не платите.

    Где взять

    Используйте uClibc++. У этой библиотеки есть одна особенность: std::map и std::set реализованы поверх вектора, поэтому при вставках и удалениях итераторы инвалидируются. Кроме того, у них иная сложность. В документации автор подробно описывает, почему он так сделал.

    Для чего нужен C++

    Это тема для отдельной статьи, которую я готов написать, если вам это интересно. Если кратко, то при грамотном использовании C++ позволяет писать столь же эффективные решения, что и решения на C, но при этом получать более читаемый и более безопасный за счет проверок компилятора код. А механизм шаблонов позволяет писать эффективные реализации обобщенных алгоритмов, что проблематично на C. А еще я к нему привык. В любом случае, я крайне прошу воздержаться от дискуссии на эту тему сейчас.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 33
    • 0
      Благодарю, полезно.
    • +1
      Супер. Ещё не хватает бутстрап-кода, который вызовет все конструкторы/деструкторы для статических переменных (они лежат в отдельной секции elf .init/.fini), но он нагугливается
      • 0
        Я правильно понимаю, что под статическими переменными вы понимаете глобальные переменные?

        В таком случае, если верить FAQ avrlibc дополнительный код не нужен: «Constructors and destructors are supported though, including global ones.»
        • 0
          А, вероятно, теперь это стало доступно.
          В своё время, когда я писал под AVR, то ли по причине отсутствия функционала, то ли по причине переписанной кастомной stdlib и бутстрапперта, это всё не работало, и приходилось закатывать Солнце вручную
          • 0
            Да, судя по спискам рассылки, раньше конструкторы и деструкторы не работали.
      • +2
        Не могу удержаться от того, чтобы не запостить сюда тролейбус.жпг и от того. чтобы сказать что на контроллере с++ совершенно не нужен.

        Ибо там слишком низкоуровневые системы. Полагаю, что всё это будет во первых безумно кушать и без того миниатюрной оперативной памяти, а во вторых, чистый си наиболее приближен к ассемблеру. Что позволяет достаточно легко дизассемблировать программу и посмотреть сбойный кусок, как такое обстоит с с++? Полагаю, что граблей можно хватить просто вагон.
        • +1
          Полагаю, что всё это будет во первых безумно кушать и без того миниатюрной оперативной памяти

          Вы думаете, а я мерял. И серьезные люди из ISO в 2006 году меряли и пришли к выводам, что причин C++ коду потреблять больше памяти или работать медленее, чем C код, нет. Исследование проводилось как раз на тему применимости C++ для встраиваемых систем.

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

          Точно также. Структуры C++ совершенно аналогичны структурам данных, которые пишут C программисты руками. При нормально знании языка я без труда понимаю, во что он превращается. Правила не сложные.
          • 0
            Тогда мы просим подробный пост о си и с++ на авр с ассемблерными выкладками по возможности, прочту с громадным удовольствием!
            • 0
              Совсем подробный — это TR18015 — 202 страницы с разбором эффективности (скорость, потребление RAM, ROM, скорость компиляции, затраты на разработку и поиск ошибок) C++. В том числе там идет сравнение с C. Дается разбор действий компилятора, необходимых для реализации той или иной фичи, а также даютя реальные замеры производительности 6ти разных компиляторов. Кроме того даются рекомендации по организации интерфейса к железу.

              Меня же хватит только на несколько экранов текста.
              • 0
                Ну я не прошу тут давать справочник :) Но общий принцип хотелось бы уловить.
        • +2
          А после всей этой херни у нас остается 20байт флеша и мы можем понтово помигать светодиодиком :)
          • 0
            А вот тут вы не правы. Я приводил оценки в статье.

            Использование чисто виртуальных методов — 20 байт флеша и 2 байта оперативки вне зависимости от количества. И то, если такие методы есть в программе.

            Статические переменные с инициализацией в рантайме — их вообще лучше не использовать. Поэтому оверхэда нет.

            placement new — это inline функции, которые будут убраны компилятором, оверхэда нет.

            прочие new, delete — не мерял, но дадут оверхэд в десяток байт, если их использовать.

            Так что я наоборот уложился в 20 байт флеша, а остальное оставил на полезные задачи.
          • +2
            Никто не спорит, что на С++ можно исхитриться писать так же эффективно, как и на Си. Вот только проблема в том, что сделав пару #include из STL и написав объектный модуль с иерархией и наследованием объектов событий (был такой протокол), мы поимели увеличение прошивки ровно в два раза с 0,7 МБ до почти 1.4 МБ. Мало того что её ещё приходится заливать дольше, так ещё и компилируется всё это добро крайне небыстро. Пришлось сделать кусок прошивки опциональным, после чего он стал крайне непопулярным и его редко кто включал в сборку.
            Функционал С++ никогда на этом уровне не был настолько уж жизненно необходим. Что за такая нелюбовь с Си, что прямо так и тянет потратиться там, где дорог каждый байтик.
            • +1
              #include из STL и написав объектный модуль с иерархией и наследованием объектов событий (был такой протокол), мы поимели увеличение прошивки ровно в два раза с 0,7 МБ до почти 1.4 МБ.

              Поэтому я не призывал везде использовать STL. Там есть сложные объекты и есть совсем простые. Сложные безусловно надо мерять и хорошо думать, прежде чем использовать. Но такая элементарщина, как std::min, которая встречается довольно часто, точно не повредит.
              Что за такая нелюбовь с Си, что прямо так и тянет потратиться там, где дорог каждый байтик.

              Ну что вы, я люблю C. Но вместо структуры и двух функций init и destroy я предпочту класс. Это полный аналог и даст такой же ассемблерный код (это я проверял), зато компилятор сам расставит вызовы деструктора.
              В C++ много вещей, которые стоят ровно столько же, сколько их аналоги на C, только предоставляют больше проверок корректности кода. А если я могу сделать что-то простыми способами на C, то с какой стати я буду делать это сложно на C++?
            • 0
              Интересно, почему компилятор заполняет vtable после вызова конструктора, а не до? Неужели ошибка дизайнера языка и последующее бремя совместимости?

              В c#, например, кусок кода с классами B и C из статьи работает.
              Да это и удобно, в конструкторе базового класса окна вызвать виртуальный GetTitle(), который определён в потомке, чтобы заполнить заголовок создаваемого окна.
              • 0
                Нет, не ошибка. Ошибка — разрешать вызывать виртуальные функции из конструктора. Дело в том, что дочерний класс в это время не инициализирован, более того, он не знает, какая из его баз уже инициализирована. Поэтому виртуальный метот в этот момент не может использовать ни одного свойства объекта, включая свойства базовых классов. Это очень сильное ограничение, выполнение которого очень трудно проконтролировать.
                А vtable заполнять не надо, он один на класс, а не на экземпляр класса. Переключение между vtable — это просто запись константы (адреса vtable) в соответсвующую ячейку.
                • 0
                  Понятно. Метод, который не имеет доступа к полям, не очень полезен.
                  Но можно было бы поменять порядок инициализации. В c#, например, сначала инициализируются члены классов (в произвольном порядке), а потом вызываются конструкторы (от предка к потомку).
                  • 0
                    Ах, я понял, почему так нельзя.
                    В с++ есть превосходный способ передать ссылку на класс-контейнер в конструктор члена, при этом базовый класс гарантировано построен:
                    class MyClass {
                        MyClass() : x(this) { }
                        MyMember x;
                    };


                    Поэтому про члены нельзя сказать, что они ничего не знают о классе и их можно инициализировать в любом порядке до конструкторов.
                    • 0
                      В с++ есть превосходный способ передать ссылку на класс-контейнер в конструктор члена, при этом базовый класс гарантировано построен:

                      Как раз наоборот, конструктор MyClass еще не отработал, а вы уже его адрес куда-то передали. Кроме того, получилась циклическая зависимость: два класса знают друг про друга. Это признак плохого дизайна: если такое появляется в коде, на него надо очень пристально посмотреть и подумать, нельзя ли иначе. Как правило, можно.
                      • +1
                        «Базовый класс гарантировано построен» — конструктор предка MyClass отработал (в примере этот базовый класс не показал)
                        • 0
                          Да, именно такой трюк используется в QT, чтобы обеспечить удаление дочерних виджетов базовым.
                    • 0
                      А зачем? Ваш пример с GetTitle() довольно страннен: отрисовка окна — это не то, что должно делаться в конструкторе. Конструктор должен сделать валидный объект, а остальное должны делать другеи методы класса. Причем если базовому классу нужны данные дочернего, то вы допустили ошибку проектирования: данными владеет не тот, кто их использует. Если заголовок есть у базового класса, то и хранить его должен он.
                      • 0
                        А теперь про порядок инициализации. Инициализировать все переменные в том порядке, в котором они лежат в памяти — это хорошо. У нас эффективно будет работать кэш из-за локальности данных и будет работать prefetch. Причем код списка инициализации дописывается в начало конструктора и мы имеем одну функцию. Если бы нужно было сначала инициализировать все свойства из списка инициализации, а затем вызывать конструкторы, надо было бы делать две отдельные функции и, соответсвенно, удвоилось бы количество вызовов.

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

                        Кроме того, работает логика, что класс — это законченная сущность, минимизирующая связность с внешними по отношению к классу данными. Поэтому классу должно хватать для инициализации того, что передано через аргументы. Иначе возникает неявные связи, усложняющие логику программы, ухудшающие её читаемость и в конечном итоге приводящие к ошибкам.
                  • +1
                    Очень недурно, впервые за многое время интересная статья на тему low-level C++.
                    • 0
                      А почему без placement new нельзя сделать кольцевой буфер? Может я чего-то не понимаю, но ведь кольцевой буфер — это просто массив, «закольцовывается» он чисто логически.
                      • 0
                        Но он же может быть и пустым. Это значит, что надо сначала выделить место под весь буффер, но не создавать там объекты. Даже не вызывать конструкторы по-умолчанию. А потом, по мере поступления объектов, класть их в буфер, вызывая placement new, а при забирании элемента из буфера надо обязательно вызывать деструктор у удаляемого элемента.
                        А «просто массив» будет хорошо работать только для POD'ов. Ну еще для классов с конструкторами по-умолчанию, не делающих в конструкторах сложных вещей, когда вам не важно в какой момент отработают конструктор и деструктор.
                        • 0
                          Я недостаточно хорошо знаю с++ и затрудняюсь представить объект со сложным конструктором, который не использует динамическую память. Вы не могли бы привести пример?
                          • 0
                            Это, как правило, объекты, представляющие различные ресурсы. Пример не из embedded мира: открытый файл, socket (например в очереди соединений, ожидающих обработки), lock. Я могу представить объект, который на время своего существования держит зажженым индикатор: например длительная операция может зажигать лампу «busy».

                            Но тут есть еще одна сторона. Без placement new мне надо заполнить буфер объектами, причем мне неоткуда взять валидные объекты. Если я напишу конструктор по-умолчанию, то я допущу существование «пустого» объекта, к тому же потрачу время на инициализацию элементов буфера, скажем, нулями. Или же я не буду писать конструктор и у меня в объектах будет храниться мусор. Это плохо тем, что в другом месте программы я могу случайно создать невалидный объект и компилятор этого не заметит и не предупредит. Вместо этого я запрещу существование невалидных объектов: если он создан, то с ним всегда можно работать. А контейнер будет иметь возможность выделить память, но не класть туда объекты.
                            • 0
                              Кстати, пример с индикатором не такой уж и странный. У функции может быть много точек выхода, поэтому проще один раз создать объект в начале и знать, что индикатор погаснет в любом случае, чем ставить вызов функции гашения индикатора перед каждым return. Кроме того, мы разделяем логику: моргание лампочкой это одна задача, а бизнес-логика — это другая. Смешивать их не надо, в том числе потому, что при переходе на другой камень я хочу переписать только аппаратно-зависимый класс моргания лампочкой, а универсальную логику оставить без изменений.
                              При этом не должно возникать overhead'а: конструктор и деструктор заинлайнятся в места вызова и превратятся в одну-две инструкции, устанавливащие бит в нужном регистре.
                              • 0
                                Спасибо, примерно понял.
                                Просто я сам юзал кольцевые буферы только для передачи данных по каким-нибудь интерфейсам; т.е. в буферах лежали просто байты (или указатели). То есть кольцевыми буферами вполне можно пользоваться и без new, хоть на чистом Си. Именно поэтому делать буфер из сложных объектов мне кажется немного странным. Но в целом логика ясна, спасибо.
                                • 0
                                  На C кольцевой буфер пишется под каждую конкретную реализацию или хранит нетипизированные блоки байт. На C++ это будет шаблон, учитывающий особенности каждого конкретного типа данных. Причем хорошая реализация для простых типов будет иметь специальную эффективную реализацию. Например move(begin, end, destination) может определить, что тип допускает побайтное копирование и скомпилироваться в такой же код, как и memscp. С поправкой на разные прототипы функций, конечно.
                                  Написать такую реализацию сложно, но зато она кладется в библиотеку и потом используется много раз. В gnu STL я такие оптимизации вижу, а вот в uclibc++ их нет, в том числе потому, что когда писался uclibc++ не все из них можно было сделать.
                                  • 0
                                    memscp — забавная опечатка. Вроде как «scp» — unix-утилита «secure copy» (связь защищена SSH), и в то же времям «mem» — не на другую машину, а из памяти в память, по зашифрованному туннелю :)

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