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

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

#include #include

struct Base { static std: unique_ptr 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: create (bool x) { if (x) return std: unique_ptr(new A ()); else return std: unique_ptr(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 #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(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(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(m); }

private: typedef void (deleter_t)(void *);

template void createObj () { new (m) T (); deleter = freeObj; }

template static void freeObj (void *p) { reinterpret_cast(p)→T::~T (); }

unsigned char m[sizeof (U)]; deleter_t *deleter; }; Таким образом, мест, требующих правки при добавлении создаваемых типов, стало не три, а два. Уже лучше, но все еще не перфект. Основная проблема осталась.Что бы решить эту задачу нужно избавиться от объединения. Но при этом сохранить предоставляемую им наглядность и возможность определять необходимый размер.

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

Нет проблем! Это же C++!

template class TypeUnion { public: // Разрешаем создание неинициализированных объектов TypeUnion () {}; // Запретим копирование TypeUnion (const TypeUnion &) = delete; // Только перемещение TypeUnion (TypeUnion &&) = default;

~TypeUnion () { // Проверяем был ли размещен какой-нибудь объект // если да, разрушаем его if (deleter) deleter (mem); }

// этот метод размещает в «объединении» объект типа T // при этом тип T должен быть перечислен среди типов указанных при создании объединения // Список аргументов args будет передан конструктору template void assign (Args&&… args) { // Проверяем на этапе компиляции возможность создания объекта в «объединении» static_assert (usize, «TypeUnion is empty»); static_assert (same_as(), «Type must be present in the types list »);

// Проверяем не размещен ли уже какой-то объект в памяти // Если размещен, освобождаем память от него. if (deleter) deleter (mem);

// В выделенной памяти создаем объект типа Т // Создаем объект, используя точную передачу аргументов new (mem) T (std: forward(args)…); // эта функция корректно разрушит инстацированный объект deleter = freeMem; }

// Получаем указатель на размещенный в «объединении» объект template T* get () { static_assert (usize, «TypeUnion is empty»); assert (deleter); // TypeUnion: assign was not called return reinterpret_cast(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 static constexpr size_t max (size_t r0, R… r) { return (r0 > max (r…) ? r0: max (r…)); }

// is_same для нескольких типов template static constexpr bool same_as () { return max (std: is_same:: value…); }

// шаблонная функция используется для разрушения размещенного в памяти объекта template static void freeMem (void *p) { reinterpret_cast(p)→T::~T (); }

// Вычисляем максимальный размер из содержащихся типов на этапе компиляции 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(); } }; Вот теперь перфект. Запись TypeUnion obj нагляднее чем union U {A a; B b;}. И ошибка с несоответствием типов будет отловлена на этапе компиляции.Полный код примера #include #include #include

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 class TypeUnion { public: // Разрешаем создание неинициализированных объектов TypeUnion () {}; // Запретим копирование TypeUnion (const TypeUnion &) = delete; // Только перемещение TypeUnion (TypeUnion &&) = default;

~TypeUnion () { // Проверяем был ли размещен какой-нибудь объект // если да, разрушаем его if (deleter) deleter (mem); }

// этот метод размещает в «объединении» объект типа T // при этом тип T должен быть перечислен среди типов указанных при создании объединения // Список аргументов args будет передан конструктору template void assign (Args&&… args) { // Проверяем на этапе компиляции возможность создания объекта в «объединении» static_assert (usize, «TypeUnion is empty»); static_assert (same_as(), «Type must be present in the types list »);

// Проверяем не размещен ли уже какой-то объект в памяти // Если размещен, освобождаем память от него. if (deleter) deleter (mem);

// В выделенной памяти создаем объект типа Т // Создаем объект, используя точную передачу аргументов new (mem) T (std: forward(args)…); // эта функция корректно разрушит инстацированный объект deleter = freeMem; }

// Получаем указатель на размещенный в «объединении» объект template T* get () { static_assert (usize, «TypeUnion is empty»); assert (deleter); // TypeUnion: assign was not called return reinterpret_cast(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 static constexpr size_t max (size_t r0, R… r) { return (r0 > max (r…) ? r0: max (r…)); }

// is_same для нескольких типов template static constexpr bool same_as () { return max (std: is_same:: value…); }

// шаблонная функция используется для разрушения размещенного в памяти объекта template static void freeMem (void *p) { reinterpret_cast(p)→T::~T (); }

// Вычисляем максимальный размер из содержащихся типов на этапе компиляции 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 Остались какие-нибудь грабли, которые я не заметил?

Спасибо за внимание!

© Habrahabr.ru