У классической реализации фабричного метода на C++ есть один существенный недостаток — используемый при реализации этого шаблона динамический полиморфизм предполагает размещение объектов в динамической памяти. Если при этом размеры создаваемых фабричным методом объектов не велики, а создаются они часто, то это может негативно сказаться на производительности. Это связанно с тем, что во первых оператор
Для решения этой проблемы было бы хорошо сохранить динамический полиморфизм (без него реализовать шаблон не получится) и при этом выделять память на стеке.
Если вам интересно, как это у меня получилось, добро пожаловать под кат.
Одна из возможных реализаций классического фабричного метода:
output:
Тут думаю, ничего комментировать не нужно. По диапазонам адресов, можно косвенно убедиться, что созданный объект действительно разместился в куче.
Теперь избавимся от динамического выделения памяти.
Как я сказал выше, мы исходим из того, что создаваемые объекты имеют небольшой размер и предлагаемый ниже вариант улучшает производительность за счет незначительного перерасхода памяти.
output:
По напечатанным адресам, вы можете видеть, что таки да. Объект разместился на стеке.
Идея здесь очень простая: мы берем объединение объектов которые будет создавать фабричный метод и с помощью него узнаем размер самого ёмкого типа. Затем выделяем на стеке память нужного размера
В принципе, на этом можно было бы и остановиться, но в полученном решении мне не нравится то что информация о создаваемых типах в классе BaseCreator присутствует в трех местах. И если нам понадобится, что бы наш фабричный метод создавал объекты еще одного типа, нам придется синхронно вносить изменения во все эти три места. При этом в случае ошибки компилятор ничего не скажет. Да и в режиме выполнения ошибка может всплыть не сразу. А если типов будет не 2-3, а 10-15 то вообще беда.
Попробуем улучшить наш класс BaseCreator
Таким образом, мест, требующих правки при добавлении создаваемых типов, стало не три, а два. Уже лучше, но все еще не перфект. Основная проблема осталась.
Что бы решить эту задачу нужно избавиться от объединения. Но при этом сохранить предоставляемую им наглядность и возможность определять необходимый размер.
А что, если бы у нас было «умное объединение», которое не просто знало бы свой размер, но и позволяло бы динамически создавать в нем объекты перечисленных в этом объединении типов? Ну и при этом, разумеется осуществляло бы контроль типов.
Нет проблем! Это же C++!
Теперь и BaseCreator выглядит куда приятнее:
Вот теперь перфект. Запись
Остались какие-нибудь грабли, которые я не заметил?
Спасибо за внимание!
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
Остались какие-нибудь грабли, которые я не заметил?
Спасибо за внимание!
