Фабричный метод без размещения в динамической памяти

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



    Одна из возможных реализаций классического фабричного метода:
    #include <iostream>
    #include <memory>
    
    struct Base
    {
        static std::unique_ptr<Base> create(bool x);
        virtual void f() const = 0;
        virtual ~Base() { std::cout << "~Base()" << std::endl;}
    };
    
    struct A: public Base
    {
        A() {std::cout << "A()" << std::endl;}
        virtual void f() const override {std::cout << "A::f\t" << ((size_t)this) << std::endl;}
        virtual ~A() {std::cout << "~A()" << std::endl;}
    };
    
    struct B: public Base
    {
        B() {std::cout << "B()" << std::endl;}
        virtual void f() const override {std::cout << "B::f\t" << ((size_t)this) << std::endl;}
        virtual ~B()  {std::cout << "~B()" << std::endl;}
    };
    
    std::unique_ptr<Base> Base::create(bool x)
    {
        if(x) return  std::unique_ptr<Base>(new A());
        else  return  std::unique_ptr<Base>(new B());
    }
    
    int main()
    {
        auto p = Base::create(true);
        p->f();
        std::cout << "p addr:\t" << ((size_t)&p) << std::endl;
        return 0;
    }
    // compile & run:
    // g++ -std=c++11 1.cpp && ./a.out
    

    output:
    A()
    A::f	21336080
    p addr:	140733537175632
    ~A()
    ~Base()
    

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

    Теперь избавимся от динамического выделения памяти.
    Как я сказал выше, мы исходим из того, что создаваемые объекты имеют небольшой размер и предлагаемый ниже вариант улучшает производительность за счет незначительного перерасхода памяти.
    #include <iostream>
    #include <memory>
    
    struct Base
    {
        virtual void f() const = 0;
        virtual ~Base() { std::cout << "~Base()" << std::endl;}
    };
    
    struct A: public Base {/* code here */};
    struct B: public Base {/* code here */};
    
    class BaseCreator
    {
        union U 
        {
            A a;
            B b;
        };
    public:
        BaseCreator(bool x) : _x(x)
        {
            if(x) (new(m) A());
            else  (new(m) B());
        }
    
        ~BaseCreator()
        {
            if(_x) {
                reinterpret_cast<A*>(m)->A::~A();
            }
            else   {
                reinterpret_cast<B*>(m)->B::~B();
            }
        }
    
        Base* operator->()
        {
            return reinterpret_cast<Base *>(m);
        }
    
    private:
        bool _x;
        unsigned char m[sizeof(U)];
    };
    
    int main(int argc, char const *argv[])
    {
        BaseCreator p(true);
        p->f();
        std::cout << "p addr:\t" << ((size_t)&p) << std::endl;
        return 0;
    }
    

    output:
    A()
    A::f	140735807769160
    p addr:	140735807769160
    ~A()
    ~Base()
    

    По напечатанным адресам, вы можете видеть, что таки да. Объект разместился на стеке.
    Идея здесь очень простая: мы берем объединение объектов которые будет создавать фабричный метод и с помощью него узнаем размер самого ёмкого типа. Затем выделяем на стеке память нужного размера unsigned char m[sizeof(U)]; и с помощью специальной формы new размещаем в ней объект new(m) A().
    reinterpret_cast<A*>(m)->A::~A(); корректно разрушает размещенный в выделенной памяти объект.

    В принципе, на этом можно было бы и остановиться, но в полученном решении мне не нравится то что информация о создаваемых типах в классе BaseCreator присутствует в трех местах. И если нам понадобится, что бы наш фабричный метод создавал объекты еще одного типа, нам придется синхронно вносить изменения во все эти три места. При этом в случае ошибки компилятор ничего не скажет. Да и в режиме выполнения ошибка может всплыть не сразу. А если типов будет не 2-3, а 10-15 то вообще беда.

    Попробуем улучшить наш класс BaseCreator
    class BaseCreator
    {
        union U 
        {
            A a;
            B b;
        };
    
    public:
        BaseCreator(bool x)
        {
            if(x) createObj<A>();
            else  createObj<B>();
        }
    
        ~BaseCreator()
        {
            deleter(m);
        }
    
        // Запретим копирование
        BaseCreator(const BaseCreator &) = delete;
        // Только перемещение
        BaseCreator(BaseCreator &&) = default;
    
        Base* operator->()
        {
            return reinterpret_cast<Base *>(m);
        }
    
    private:
        typedef void (deleter_t)(void *);
    
        template<typename T>
        void createObj()
        {
            new(m) T();
            deleter = freeObj<T>;
        }
    
        template<typename T>
        static void freeObj(void *p)
        {
            reinterpret_cast<T*>(p)->T::~T();
        }
    
        unsigned char m[sizeof(U)];
        deleter_t *deleter;
    };
    


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

    А что, если бы у нас было «умное объединение», которое не просто знало бы свой размер, но и позволяло бы динамически создавать в нем объекты перечисленных в этом объединении типов? Ну и при этом, разумеется осуществляло бы контроль типов.

    Нет проблем! Это же C++!
    template <typename ...Types>
    class TypeUnion
    {
    public:
        // Разрешаем создание неинициализированных объектов
        TypeUnion() {};
        // Запретим копирование
        TypeUnion(const TypeUnion &) = delete;
        // Только перемещение
        TypeUnion(TypeUnion &&) = default;
    
        ~TypeUnion()
        {
            // Проверяем был ли размещен какой-нибудь объект
            // если да, разрушаем его
            if(deleter) deleter(mem);
        }
    
        // этот метод размещает в "объединении" объект типа T
        // при этом тип T должен быть перечислен среди типов указанных при создании объединения
        // Список аргументов args будет передан конструктору
        template <typename T, typename ...Args>
        void assign(Args&&... args)
        {
            // Проверяем на этапе компиляции возможность создания объекта в "объединении"
            static_assert ( usize, "TypeUnion is empty" );
            static_assert ( same_as<T>(), "Type must be present in the types list " );
    
            // Проверяем не размещен ли уже какой-то объект в памяти
            // Если размещен, освобождаем память от него.
            if(deleter) deleter(mem);
    
            // В выделенной памяти создаем объект типа Т
            // Создаем объект, используя точную передачу аргументов
            new(mem) T(std::forward<Args>(args)...);
            
            // эта функция корректно разрушит инстацированный объект
            deleter = freeMem<T>;
        }
    
        // Получаем указатель на размещенный в "объединении" объект
        template<typename T>
        T* get()
        {
            static_assert ( usize, "TypeUnion is empty" );
            assert ( deleter ); // TypeUnion::assign was not called
            return reinterpret_cast<T*>(mem);
        }
    
    private:
        // функция этого типа будет использована для вызова деструктора
        typedef void (deleter_t)(void *);
    
        // Вдруг кто то захочет создать TypeUnion с пустым списком типов?
        static constexpr size_t max()
        {
            return 0;
        }
        // вычисляем максимум на этапе компиляции
        static constexpr size_t max(size_t r0)
        {
            return r0;
        }
        template <typename ...R>
        static constexpr size_t max(size_t r0, R... r)
        {
            return ( r0 > max(r...) ? r0 : max(r...) );
        }
    
        // is_same для нескольких типов
        template <typename T>
        static constexpr bool same_as()
        {
            return max( std::is_same<T, Types>::value... );
        }
    
        // шаблонная функция используется для разрушения размещенного в памяти объекта
        template<typename T>
        static void freeMem(void *p)
        {
            reinterpret_cast<T*>(p)->T::~T();
        }
    
        // Вычисляем максимальный размер из содержащихся типов на этапе компиляции
        static constexpr size_t usize = max( sizeof(Types)... );
    
        // Выделяем память, вмещающую объект наиболшего типа
        unsigned char mem[usize];
    
        deleter_t *deleter = nullptr;
    };
    


    Теперь и BaseCreator выглядит куда приятнее:
    class BaseCreator
    {
        TypeUnion<A, B> obj;
    
    public:
        BaseCreator(bool x)
        {
            if(x) obj.assign<A>();
            else  obj.assign<B>();
        }
    
        // Запретим копирование
        BaseCreator(const BaseCreator &) = delete;
        // Только перемещение
        BaseCreator(BaseCreator &&) = default;
    
        Base* operator->()
        {
            return obj.get<Base>();
        }
    };
    

    Вот теперь перфект. Запись TypeUnion<A, B> obj нагляднее чем union U {A a; B b;}. И ошибка с несоответствием типов будет отловлена на этапе компиляции.

    Полный код примера
    #include <iostream>
    #include <memory>
    #include <cassert>
    
    struct Base
    {
        virtual void f() const = 0;
        virtual ~Base() {std::cout << "~Base()\n";}
    };
    
    struct A: public Base
    {
        A(){std::cout << "A()\n";}
        virtual void f() const override{std::cout << "A::f\n";}
        virtual ~A() {std::cout << "~A()\n";}
    };
    struct B: public Base
    {
        B(){std::cout << "B()\n";}
        virtual void f() const override{std::cout << "B::f\n";}
        virtual ~B() {std::cout << "~B()\n";}
        size_t i = 0;
    };
    
    
    
    template <typename ...Types>
    class TypeUnion
    {
    public:
        // Разрешаем создание неинициализированных объектов
        TypeUnion() {};
        // Запретим копирование
        TypeUnion(const TypeUnion &) = delete;
        // Только перемещение
        TypeUnion(TypeUnion &&) = default;
    
        ~TypeUnion()
        {
            // Проверяем был ли размещен какой-нибудь объект
            // если да, разрушаем его
            if(deleter) deleter(mem);
        }
    
        // этот метод размещает в "объединении" объект типа T
        // при этом тип T должен быть перечислен среди типов указанных при создании объединения
        // Список аргументов args будет передан конструктору
        template <typename T, typename ...Args>
        void assign(Args&&... args)
        {
            // Проверяем на этапе компиляции возможность создания объекта в "объединении"
            static_assert ( usize, "TypeUnion is empty" );
            static_assert ( same_as<T>(), "Type must be present in the types list " );
    
            // Проверяем не размещен ли уже какой-то объект в памяти
            // Если размещен, освобождаем память от него.
            if(deleter) deleter(mem);
    
            // В выделенной памяти создаем объект типа Т
            // Создаем объект, используя точную передачу аргументов
            new(mem) T(std::forward<Args>(args)...);
            
            // эта функция корректно разрушит инстацированный объект
            deleter = freeMem<T>;
        }
    
        // Получаем указатель на размещенный в "объединении" объект
        template<typename T>
        T* get()
        {
            static_assert ( usize, "TypeUnion is empty" );
            assert ( deleter ); // TypeUnion::assign was not called
            return reinterpret_cast<T*>(mem);
        }
    
    private:
        // функция этого типа будет использована для вызова деструктора
        typedef void (deleter_t)(void *);
    
        // Вдруг кто то захочет создать TypeUnion с пустым списком типов?
        static constexpr size_t max()
        {
            return 0;
        }
        // вычисляем максимум на этапе компиляции
        static constexpr size_t max(size_t r0)
        {
            return r0;
        }
        template <typename ...R>
        static constexpr size_t max(size_t r0, R... r)
        {
            return ( r0 > max(r...) ? r0 : max(r...) );
        }
    
        // is_same для нескольких типов
        template <typename T>
        static constexpr bool same_as()
        {
            return max( std::is_same<T, Types>::value... );
        }
    
        // шаблонная функция используется для разрушения размещенного в памяти объекта
        template<typename T>
        static void freeMem(void *p)
        {
            reinterpret_cast<T*>(p)->T::~T();
        }
    
        // Вычисляем максимальный размер из содержащихся типов на этапе компиляции
        static constexpr size_t usize = max( sizeof(Types)... );
    
        // Выделяем память, вмещающую объект наиболшего типа
        unsigned char mem[usize];
    
        deleter_t *deleter = nullptr;
    };
    
    class BaseCreator
    {
        TypeUnion<A, B> obj;
    
    public:
        BaseCreator(bool x)
        {
            if(x) obj.assign<A>();
            else  obj.assign<B>();
        }
    
        // Запретим копирование
        BaseCreator(const BaseCreator &) = delete;
        // Только перемещение
        BaseCreator(BaseCreator &&) = default;
    
        Base* operator->()
        {
            return obj.get<Base>();
        }
    };
    
    int main(int argc, char const *argv[])
    {
        BaseCreator p(false);
        p->f();
    
        std::cout << "sizeof(BaseCreator):" << sizeof(BaseCreator) << std::endl;
        std::cout << "sizeof(A):" << sizeof(A) << std::endl;
        std::cout << "sizeof(B):" << sizeof(B) << std::endl;
        return 0;
    }
    //
    // clang++ -std=c++11 1.cpp && ./a.out
    



    Остались какие-нибудь грабли, которые я не заметил?

    Спасибо за внимание!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 49

      +9
      Фактически, вы изобрели вариант, вроде boost::variant. А использованная конструкция называется tagged union. Для упрощения создания таких штук в C++11 предусмотрели std::aligned_union (кстати, вопрос выравнивания у вас не затронут).
        +5
        Ну и вообще, в C++11 есть relaxed unions: разрешено иметь объединения, содержащие любые типы, но необходимо в ручную вызывать placement new и деструктор (собственно, вы такое объединение объявили, но использовали почему-то только для вычисления размера, но не напрямую для хранения элементов). Собственно, массив mem не нужен, а достаточно поместить объединение в класс. Почитать
          0
          В этом случае как раз и получится то, что у меня представлено во втором варианте. Дело в том, что 1) доступ к членам union осуществляется по их именам, а не типам, и 2) мы не сможем создать объект такого объединения, если он будет содержать более одного не POD типа с нетривиальным конструктором. Попробуйте сами.
            0
            Как минимум в GCC 4.8 и 4.9 работает. А вы какой компилятор используете?
              0
              У меня тоже компилируется. Я использую clang 3.2-7 и gcc 4.8.1. Ваш пример с Variant я посмотрю. Пока просто не успел. Обязательно разберусь с этим подходом.
              0
              А на счёт доступа по именам — это решается с помощью рекурсивного объединения. По ссылке, которую я привел, описан весь процесс создания Variant таким методом.
              +1
              Сделал перевод статьи и оставляю этот комментарий в качестве перекрёстной ссылки для заинтересовавшихся.
                0
                Круто! Вы успели перевести быстрее чем я прочитать.
              0
              Спасибо за интересные ссылки.
              По поводу выравнивания. Так ведь sizeof возвращает размер с учетом выравнивания. Так что не вижу с этим проблемы. Или какой то еще подвох здесь есть?
              На счет std::aligned_union согласен, вместо unsigned char mem[usize] лучше использовать его.
                +8
                sizeof возвращает размер с учетом выравнивания после объекта.
                А для корректной работы может понадобиться выравнивать начало размещения объекта в памяти по границе, которая не совпадает со стандартным выравниванием начала массивов char.
                  0
                  Ага понял. Тогда да, — это грабли. Ладно, вечерком посмотрю, подправлю.
                +2
                Фактически, вы изобрели вариант, вроде boost::variant.


                С boost::variant код выглядел бы следующим образом и имел бы следующие плюсы:
                class BaseCreator
                {
                    // boost::none_t - чтобы иметь пустое состояние и конструктор по умолчанию без накладных расходов
                    typedef boost::variant<boost::none_t, A, B> obj_t;
                    obj_t obj;
                
                public:
                    BaseCreator() = default; // noexcept
                
                    BaseCreator(bool x)
                        : obj(x ? obj_t(A()) : obj_t(B()))
                    {}
                
                    // Можно не запрещать копирование :)
                    BaseCreator(const BaseCreator &) = default;
                    // Перемещение
                    BaseCreator(BaseCreator &&) = default;
                
                    Base* operator->()
                    {
                        // polymorphic_get был добавлен в Boost 1.56. Рекомендую к употребелению! Классная штука.
                        //
                        // Если это единственное место где используется Base, то 
                        // деструктор можно не делать виртуальным.
                        //
                        // Кстати, Ваш пример будет некорректно работать в случае сложного множественного наследования:
                        // так reinterpret_cast<T*>(mem); не справится с struct A: virtual Base1, virtual Base {...}
                        return &boost::polymorphic_get<Base>(obj);
                    }
                };
                

                  +1
                  Код примера и с обычным множественным наследованием работать не будет, достаточно, чтобы адрес базового класса не совпадал с адресом начала объекта.
                    0
                    Да, получилось весьма симпатично.
                  +3
                  Какая-то странная фабрика. Может вернуть только один объект за раз. И конструктор вызовется непонятно когда. Вызывающий не может детерминировано удалить его. Почему вообще не сделать пул, если уж на то пошло?
                  Не представляю, где его можно на практике применить. Может приведете пример?
                    0
                    >И конструктор вызовется непонятно когда.
                    Имел в виду деструктор.
                      0
                      Может вернуть только один объект за раз

                      Потому что это не «фабрика», а «фабричный метод». Собственно переделать всю конструкцию в фабрику по общему принципу тоже можно. Просто на примере фабричного метода проще проиллюстрировать подход.
                      Деструктор явно вызывать не нужно, т.к.стековые объекты существуют лишь в границах содержащего их блока. Собственно с классическим фабричным методом тоже самое, если он возвращает не сырой указатель, а, как положено, unique_ptr.
                      Что касается применения, то оно ни чем не отличается от применения обычного фабричного метода с динамическим размещением. См. первый попавшийся пример из гугла
                        0
                        Деструктор вызвать все-таки нужно. Не нужно освобождать память.
                          +1
                          Так он и вызывается автоматически. Во всех примерах. Гляньте еще разок.
                          0
                          Деструктор нельзя не вызывать. Может там ресурсы какие-то освобождаются.

                          >Потому что это не «фабрика», а «фабричный метод».
                          Ну и что? Creator может несколько объектов запросить.
                            –1
                            Да потому что это не фабричный метод, а этакой pimpl
                      0
                      Очень удивился, увидев non-POD типы в union, но потом сообразил, что видимо речь идет про С++11.
                        +8
                        То есть std::unique_ptr, override и -std=c++11 вас не смутили в первом же сниппете? :)
                        +6
                        У вас большие проблемы с выравниванием и копированием/переносом.

                        Нельзя просто взять и скопировать объект в C++, скопировав его данные. Нужно еще вызвать его конструктор копирования.

                        И вообще нужно просто использовать small obejct allocator-ы, и не париться.
                          0
                          Копирование запрещено. На счет конструктора перемещения, похоже, вы правы. С TypeUnion(TypeUnion &&) = default я поторопился. Еще одни грабли.
                            –1
                            Сейчас еще раз подумал. Мы же добиваемся такого же поведения, как и при использовании динамического выделения. Так что, запрещенный конструктор копирования и дефолтный конструктор переноса — то что надо. Собственно unique_ptr так и работает.
                            Таким образом, с копированием/переносом проблем нет.
                              +1
                              Не добиваемся мы. Для того, чтобы добиться этого, надо чтобы объект не перемешался при перемещении `BaseCreator`-а. `unique_ptr` работает именно _так_, а ваш код — не так.
                                0
                                Да, каюсь. Глупость ляпнул.
                                +1
                                Дефолтный конструктор перемещения BaseCreator просто вызывает конструктор перемещения TypeUnion, и это адекватное поведение. Но вот конструктор перемещения TypeUnion, вместо того чтобы вызвать конструктор перемещения того объекта, который в нем в данный момент хранится, тоже объявлен дефолтным, а это значит, что он просто копирует (через memcpy) внутренний буфер и указатель. Если в TypeUnion будет хранится, например, unique_ptr, то это приведет к тому, что у вас станет два unique_ptr'а, владеющие одной и той же памятью, в результате чего, например, после вызова деструкторов обоих TypeUnion один и тот же участок памяти будет освобожден дважды.
                              +2
                              Интересный факт, что ваш код (который под спойлером "Полный код примера") крашится во время работы, если скомпилировать его на GCC 4.9.0 c флагом -O2: coliru.stacked-crooked.com/a/1742a8366403abef Если компилировать clang++, GCC 4.8.2 или понизить флаг до -O1, то все работает как надо. Не готов сказать баг ли это компилятора, или у вас какое-то хитрое UB в коде, проблема в вызове p->f() и она как-то связана с инлайном, потому что с ключами -O2 -fno-inline все начинает работать.
                                0
                                Я думаю UB более вероятно :)
                                На моих clang 3.2-7 и gcc 4.8.1 я воспроизвести не могу. Но по вашей ссылке видно, что выход происходит в строке 72
                                   assert ( deleter ); // TypeUnion::assign was not called
                                Т.е. каким то образом deleter == nullptr. Попробую найти GCC 4.9.0 и отдебажить.
                                Еще чуть выше мне подсказали о возможных проблемах с выравниванием. Может как то с этим связано. Короче, надо будет разобраться.
                                  0
                                  assert там не виноват, если его закомментировать, все равно будет падать. Выравнивание я поправлял, не помогло.
                                    0
                                    Ну да assert просто сигнализирует, что не был вызван assign. Который инстацирует объект. Если объект не инстацирован, то падать оно конечно будет т.к. в памяти не то что мы ожидаем. Просто такого там быть не должно. Но в g++-4.9.2 проблема не воспроизводится.
                                  +1
                                  Только что прогнал по g++-4.9.2 c разными флагами. Все ок. Никаких проблем.
                                    0
                                    Странно это, извините за беспокойство. Просто хотел кое-что проверить, закинул ваш пример в онлайн-компилятор, а там такое вот…
                                      +2
                                      Да нет, все нормально. Мне самому интересно. Сижу вот до сих пор дебажу прямо по вашей ссылке. Надо же выяснить в чем там дело.
                                        +2
                                        Если все же найдете проблему, не забудьте поделиться с общественностью.
                                          –1
                                          Видимо, все дело тут в некорректном reinterpret_cast в функции get, который приводит к UB, как сообщают нам antoshkka и lemelisk чуть выше
                                            +1
                                            Он некорректен только в случае множественного наследования, то есть когда у одного класса сразу несколько базовых. У вас в примере таких классов нет. С множественным наследованием проблема в том, что когда у объекта несколько базовых классов, то адрес одного из них будет не совпадать с адресом начала объекта, и для корректного преобразования нужно будет не только поменять тип указателя, а ещё и сдвинуть его на некоторую величину.
                                              0
                                              Вы сохранили B*, а прочитали Base* — это UB, по определению. А в нормальных программах не бывает UB. А раз тут нет UB, значит этот код никогда не выполнится. А это возможно только тогда, когда assert всегда срабатывает. А раз assert всегда срабатывает, то и не фиг его проверять. Именно так и поступил gcc, там __assertion_failed вызывается безусловно. Можете посмотреть вывод ассембелра.
                                                +1
                                                А почему это UB? Ведь Base — базовый класс B. Если заменить obj.get<Base>(); на obj.get<B>(); от краша все равно не спасает.
                                                  +1
                                                  Хм. Возможно, бага компилятора в этом месте. Потому что замена , return reinterpet_cast<...> на return new B() таки от креша спасет, так что я уверен, что логика его именно в том, что reinterpet_cast приводит к UB.
                                            0
                                            Обязательно. Но вчера я так и не нашел причину. Сложность еще и в том, что «дебажить» приходится в браузере, что весьма необычно для меня. (у меня нет g++-4.9.0, а только g++-4.9.2 и g++-4.8.1)
                                      +2
                                      Нашел баг!!! coliru.stacked-crooked.com/a/d919304815452d35
                                      Все таки не зря encyclopedist с самого начала сказал о проблемах с выравниванием, а я вместо того что бы устранить грабли принялся дебажить изначально дефективный код.
                                      Если кратко, как исправлен баг:
                                      Добавлено
                                      template <std::size_t Len, typename ...Types>
                                      struct aligned_union {
                                          static constexpr size_t max(size_t r0) {return r0;}
                                          template <typename ...R> static constexpr size_t max(size_t r0, R... r) {return ( r0 > max(r...) ? r0 : max(r...) );}
                                          static constexpr std::size_t alignment_value = max(alignof(Types)...);
                                          struct type { alignas(alignment_value) unsigned char _[max(Len, sizeof(Types)...)]; };
                                      };
                                      

                                      т.к. в этой версии gcc, видимо, std::aligned_union уже удален.
                                          unsigned char mem[usize];
                                      
                                      заменено на
                                          typedef typename aligned_union<usize, Types...>::type aligned_type;
                                          aligned_type _storage;
                                          aligned_type *mem = &_storage;
                                      

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

                                      В моем случае можно было поступить и проще. Просто заменить unsigned char mem[usize]; на
                                      alignas(max(alignof(Types)...)) unsigned char mem[usize];
                                      
                                        +1
                                        Нет, дело не в выравнивании (я его исправлял, не помогает), вы добавили лишний член — указатель mem, просто чтобы хранить в нем адрес другого члена этого же объекта (_storage), и магическим образом это помогло. Замените в функции get return static_cast<T*>(static_cast<void*>(mem)); на return static_cast<T*>(static_cast<void*>(&_storage)); и снова начнет крашится.
                                          +2
                                          Не, это не считается. Вы слишком сильно переписали код.

                                          Если заменить ваш unsigned char mem[usize]; на alignas(max(alignof(Types)...)) unsigned char mem[usize];, то делу это не поможет, все равно будет падать. Так что проблема не в alignment-е. Более того, если написать
                                          unsigned char mem_s[usize];
                                          void* mem = mem_s;
                                          

                                          то этого будет достаточно, чтобы компилятор не включал дуркуоптимизацию. Но дело в том, что если компилятор шалит, то либо у вас UB, либо в компиляторе ошибка. Когда вы так сильно переписали код, то в первом случае вы могли только спрятать UB, и поэтому это не может считаться решением пока вы не ответили, что конкретно в вашем коде было не так.
                                            +2
                                            aligned_union не удален, он наоборот, ещё не добавлен. Он будет добавлен только в GCC версии 5. (Я при написании первого комментария, честно говоря, подзабыл о том, что оно ещё недоступно в GCC, хотя сам несколько мес. назад сталкивался с этим)
                                          0
                                          Хранение объектов в «куче» — накладные расходы. Если хочется объекты переменной длины размещать в стеке, то можно ещё использовать такую технику: «Размещение объектов переменной длины с использованием двух стеков», «Реализация двухстековой модели размещения данных». Правда, она требует ассемблерных вставок.
                                            0
                                            Насколько я понимаю, никакого «особого, аппаратного стека» не существует. Просто call, ret, enter, leave, push, pop неявно используют ebp и esp. Поэтому и ассемблерных вставок в общем-то не надо, чтобы иметь глобальный динамический стек под объекты.

                                            Как самый простой вариант: alloca() — неявно выделяет память прямо на «родном» esp-стеке. Освобождает при выходе из функции. Деструкторы можно навесить, завернув такие объекты в умный указатель. Однако, alloca() considered harmful, потому что обычно у стека весьма скромный объём. Поэтому чуть более сложный вариант: засунуть в thread-local storage указатель на кусок глобальной памяти под собственный стек и написать свою alloca(), которая выделяет память оттуда.

                                          Only users with full accounts can post comments. Log in, please.