У классической реализации фабричного метода на 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::ft" << ((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::ft" << ((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::fn";}
virtual ~A() {std::cout << "~A()n";}
};
struct B: public Base
{
B(){std::cout << "B()n";}
virtual void f() const override{std::cout << "B::fn";}
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
Остались какие-нибудь грабли, которые я не заметил?
Спасибо за внимание!
Автор: rotor