Фабричный метод без размещения в динамической памяти
У классической реализации фабричного метода на C++ есть один существенный недостаток — используемый при реализации этого шаблона динамический полиморфизм предполагает размещение объектов в динамической памяти. Если при этом размеры создаваемых фабричным методом объектов не велики, а создаются они часто, то это может негативно сказаться на производительности. Это связанно с тем, что во первых оператор new не очень эффективен при выделении памяти малого размера, а во вторых с тем что частая деаллокация небольших блоков памяти сама по себе требует много ресурсов.Для решения этой проблемы было бы хорошо сохранить динамический полиморфизм (без него реализовать шаблон не получится) и при этом выделять память на стеке.Если вам интересно, как это у меня получилось, добро пожаловать под кат.Одна из возможных реализаций классического фабричного метода:
#include
struct Base
{
static std: unique_ptr
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
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
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(m)→A::~A (); } else { reinterpret_cast(m)→B::~B (); } }
Base* operator→()
{
return reinterpret_cast
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(m)→A::~A (); корректно разрушает размещенный в выделенной памяти объект.В принципе, на этом можно было бы и остановиться, но в полученном решении мне не нравится то что информация о создаваемых типах в классе BaseCreator присутствует в трех местах. И если нам понадобится, что бы наш фабричный метод создавал объекты еще одного типа, нам придется синхронно вносить изменения во все эти три места. При этом в случае ошибки компилятор ничего не скажет. Да и в режиме выполнения ошибка может всплыть не сразу. А если типов будет не 2–3, а 10–15 то вообще беда.
Попробуем улучшить наш класс BaseCreator
class BaseCreator { union U { A a; B b; };
public: BaseCreator (bool x) { if (x) createObj(); else createObj(); }
~BaseCreator () { deleter (m); }
// Запретим копирование BaseCreator (const BaseCreator &) = delete; // Только перемещение BaseCreator (BaseCreator &&) = default;
Base* operator→()
{
return reinterpret_cast
private: typedef void (deleter_t)(void *);
template
template
unsigned char m[sizeof (U)]; deleter_t *deleter; }; Таким образом, мест, требующих правки при добавлении создаваемых типов, стало не три, а два. Уже лучше, но все еще не перфект. Основная проблема осталась.Что бы решить эту задачу нужно избавиться от объединения. Но при этом сохранить предоставляемую им наглядность и возможность определять необходимый размер.
А что, если бы у нас было «умное объединение», которое не просто знало бы свой размер, но и позволяло бы динамически создавать в нем объекты перечисленных в этом объединении типов? Ну и при этом, разумеется осуществляло бы контроль типов.
Нет проблем! Это же C++!
template
~TypeUnion () { // Проверяем был ли размещен какой-нибудь объект // если да, разрушаем его if (deleter) deleter (mem); }
// этот метод размещает в «объединении» объект типа T
// при этом тип T должен быть перечислен среди типов указанных при создании объединения
// Список аргументов args будет передан конструктору
template
// Проверяем не размещен ли уже какой-то объект в памяти // Если размещен, освобождаем память от него. if (deleter) deleter (mem);
// В выделенной памяти создаем объект типа Т
// Создаем объект, используя точную передачу аргументов
new (mem) T (std: forward
// Получаем указатель на размещенный в «объединении» объект
template
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
// is_same для нескольких типов
template
// шаблонная функция используется для разрушения размещенного в памяти объекта
template
// Вычисляем максимальный размер из содержащихся типов на этапе компиляции static constexpr size_t usize = max (sizeof (Types)…);
// Выделяем память, вмещающую объект наиболшего типа unsigned char mem[usize];
deleter_t *deleter = nullptr; }; Теперь и BaseCreator выглядит куда приятнее:
class BaseCreator { TypeUnion obj;
public: BaseCreator (bool x) { if (x) obj.assign(); else obj.assign(); }
// Запретим копирование BaseCreator (const BaseCreator &) = delete; // Только перемещение BaseCreator (BaseCreator &&) = default;
Base* operator→()
{
return obj.get
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
~TypeUnion () { // Проверяем был ли размещен какой-нибудь объект // если да, разрушаем его if (deleter) deleter (mem); }
// этот метод размещает в «объединении» объект типа T
// при этом тип T должен быть перечислен среди типов указанных при создании объединения
// Список аргументов args будет передан конструктору
template
// Проверяем не размещен ли уже какой-то объект в памяти // Если размещен, освобождаем память от него. if (deleter) deleter (mem);
// В выделенной памяти создаем объект типа Т
// Создаем объект, используя точную передачу аргументов
new (mem) T (std: forward
// Получаем указатель на размещенный в «объединении» объект
template
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
// is_same для нескольких типов
template
// шаблонная функция используется для разрушения размещенного в памяти объекта
template
// Вычисляем максимальный размер из содержащихся типов на этапе компиляции static constexpr size_t usize = max (sizeof (Types)…);
// Выделяем память, вмещающую объект наиболшего типа unsigned char mem[usize];
deleter_t *deleter = nullptr; };
class BaseCreator { TypeUnion obj;
public: BaseCreator (bool x) { if (x) obj.assign(); else obj.assign(); }
// Запретим копирование BaseCreator (const BaseCreator &) = delete; // Только перемещение BaseCreator (BaseCreator &&) = default;
Base* operator→()
{
return obj.get
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 Остались какие-нибудь грабли, которые я не заметил?
Спасибо за внимание!