Привет!
Я очень новичок в C++, программирую в общем исключительно ради своего удовольствия (причём иногда для несколько экзотичных платформ), не читал теоретических книжек, в процессе написания активно использую гугл, Stack Overflow и интуицию, а ещё придерживаюсь мнения, что C++ знать невозможно.
Надеюсь, это снимет некоторые вопросы и предотвратит удивлённые взгляды. :)
Тем не менее, однажды писал (на плюсах и с Qt'ом) я в своё свободное время свой очередной велосипедик, и тут мне подумалось, что неплохо было бы в это место ввинтить некий механизм, который, как оказалось, некоторые назвают фабрикой объектов. Причём, как я понял, существует более общий паттерн проектирования, называемый абстрактной фабрикой объектов, суть которой я не уловил, а также более простая абстракция, суть которой сводится к тому, что объект знает о ряде пар класс–идентификатор, а затем он может создавать экземпляры классов по их идентификатору (в простейшем случае этот идентификатор — строка, но может быть удобно использовать, например, перечисление). Когда я более-менее понял, что я хочу (то есть, именно последнее), я полез искать готовые, красивые решения, которые меня устроили бы, но, изнасиловав почти весь гугл, что удивительно, не нашёл такового.
В результате, потратив непозволительное количество времени на такую чепуху, я собрал из кучи мест (добравшись до оставшейся части гугла) свой неповторимый и, как мне кажется, всё же элегантный небольшой (действительно небольшой… я даже сомневаюсь в нужности целого поста...) велосипед, успев разобраться много с чем, в том числе с шаблонными классами, шаблонными функциями, специализацией и даже variadic templates и functions'ами из C++11.
Оговоримся, чего именно я хочу. Во-первых, естественно, я прочитал статью читателя rtorsten Ставим объекты на поток, паттерн фабрика объектов, но смысл всего-всего происходящего до меня не доходит до сих пор, реализация кажется немного перегруженной, а способа передать аргументы конструктору при создании очередного объекта я не вижу. Можно сказать, этот пост — попытка сделать примерно то же самое, но лучше. Ну, и во-вторых, чего я хотел и чего я добился (надо же, совпало :)): предположим, у нас есть базовый класс BaseClass и два непосредственных наследника от него: Derived1 и Derived2, экземпляры которых мы хотим создавать фабрикой.
Вышеупомянутое как раз и покрывается той статьёй, но я также хочу
- создавать экземпляры классов с различными аргументами конструктора;
- иметь одну единственную реализацию фабрики, для любого базового класса и любого количества и типов аргументов конструктора.
То есть как-то так:
#include <iostream>
#include <string>
using namespace std;
class Animal{
public:
Animal(bool isAlive,string name) : isAlive(isAlive),name(name){};
bool isAlive;
string name;
virtual string voice() const=0;
};
class Dog : public Animal{
public:
using Animal::Animal;
string voice() const{
return this->isAlive?
"Woof! I'm "+this->name+"n":
"";
}
};
class Cat : public Animal{
public:
using Animal::Animal;
string voice() const{
return this->isAlive?
"Meow, I'm "+this->name+"n":
"";
}
};
int main(void){
GenericObjectFactory<string,Animal,bool,string> animalFactory;
animalFactory.add<Dog>("man's friend");
animalFactory.add<Cat>("^_^");
Animal *dog1=animalFactory.get("man's friend")(true,"charlie");
Animal *dog2=animalFactory.get("man's friend")(false,"fido");
Animal *cat =animalFactory.get("^_^")(true,"begemoth");
cout << dog1->voice()
<< dog2->voice()
<< cat ->voice();
return 0;
}
Таким образом, сначала создаётся фабрика animalFactory из шаблонного класса GenericObjectFactory, в качестве первого обязательного параметра которого выступает тип ключей (в этом примере — строка, однако также может быть сподручно использовать что-нибудь другое, типа целочисленного значения или перечисления) контейнера типа map, где значениями являются классы, которые потом необходимо будет добавить в фабрику, чтобы она могла их создавать; в качестве второго обязательного параметра — базовый класс этих классов (а они должны быть унаследованы от одного класса); оставшиеся два параметра, которых может быть любое количество (включая ноль), являются типами аргументов конструктора классов, создаваемых фабрикой, по очереди.
Затем можно добавлять классы в фабрику, вызывая шаблонную функцию-член add, указав в качестве первого и единственного параметра, собственно, класс, который фабрике необходимо зарегистрировать, а в качестве первого и единственного аргумента — идентификатор этого класса, в нашем случае — строка (определено первым параметром шаблонного класса).
Затем начинается интересное. Функция-член get принимает первым и единственным аргументом строку-идентификатор, а возвращает так называемый instantiator найденного по этому идентификатору класса. Instantiator — это такая функция, которая примет набор аргументов (в количестве и типов, указанных при специализации фабрики) и создаст экземпляр нужного класса, передав при этом их конструктору. Своего рода прокси, который нужен потом объясню зачем. Очень похоже на то, как будто бы был возвращён сам конструктор. :) Вернёт instantiator при этом указатель на созданный объект, но указатель этот имеет тип не унаследованного, а базового класса. При большом желании его потом можно dynamic_cast'нуть.
Кстати, ничего не мешает сохранить данный instantiator где-нибудь:
auto catInstantiator=animalFactory.get("^_^");
, а уже объекты создать попозже, когда надо будет:
Animal *cat=catInstantiator(true,"delayed cat");
Animal *cat=catInstantiator(false,"dead delayed cat");
:)
Но ладно, в конце концов это было очевидно. Как очевидно и то, что я пишу эту заметку не матёрым плюсистам, а новичкам, которым зачем-то захотелось себе фабрику. :)
Посмотрим на, собственно, реализацию фабрики. Я намеренно вырезал всё, без чего оно бы не работало, включая всякие проверки на наличие или отсутствие класса в фабрике при создании или регистрации и тому подобную логику и всякие другие удобства (вернее, мне лень было переносить это с Qt на STL), благо никакого труда дописать это не составляет. Но а вообще я ещё считаю, что код примера должен быть максимально прост и очевиден, без этих, безусловно, неизбежных дьявольских деталей: так легче понимать, а именно это сейчас и нужно.
- #include <map>
- template<class ID,class Base,class ... Args> class GenericObjectFactory{
- private:
- typedef Base* (*fInstantiator)(Args ...);
- template<class Derived> static Base* instantiator(Args ... args){
- return new Derived(args ...);
- }
- std::map<ID,fInstantiator> classes;
- public:
- GenericObjectFactory(){}
- template<class Derived> void add(ID id){
- classes[id]=&instantiator<Derived>;
- }
- fInstantiator get(ID id){
- return classes[id];
- }
- };
И всё! Разве не эленгантно? Мне кажется, я превзошёл решение от rtorsten, предложенное в его статье как минимум раза в три.
Итак, что здесь происходит:
- Первая строка: всё, что нужно, это какой-либо map-подобный контейнер. В данном случае берём STL-евский. Но и кьютовый QMap тоже работает без проблем.
- Третья строка: определение шаблона. Про волшебные три точки (variadic templates) можно почитать в гугле, на википедии, или даже на хабре (респект читательу FlexFerrum).
- Пятая: определение типа fInstantiator. Именно указатель на функцию, возвращающую указатель на объект типа Base (шаблонный параметр), и принимающую аргументы типов Args… (тоже шаблонный параметр, но их может быть произвольное количество, см. variadic templates ↑) возвращает функция-член get (строчка 16).
- Шестая: как раз такая функция, тот самый instantiator, про который мы говорили, при этом шаблонная. Это нужно для того, чтобы эту функцию можно было специализировать добавляемым в фабрику классом Derived, что делает шаблонная функция-член void add<class Derived>, а затем взять её, специализированной функции адрес (которая создаёт объекты уже только данного класса), и поместить в контейнер std::map classes как значение, где ключ — аргумент функции void add<class Derived> типа ID (первый параметр шаблона фабрики). Instantiator объявлен как static для простоты, но если вам очень нужно, можно воспользоваться так называемыми member function pointers'ами.
- На Седьмой строчке происходит, собственно, создание объекта, и конструктору этого объекта передаются аргументы в количестве и типах, заданных при специализации фабрики, а значения их наш instantiator, собственно, принимает аргументами. Разумеется, в таком же количестве и типах, и разумеется, это всё известно во время компиляции, и если ни один из конструкторов какого-нибудь из добавляемых в фабрику классов не принимает аргументы таких типов, компилятор бросит ошибку.
- Девятая строчка: контейнер, в котором фабрика сохраняет добавленные в неё классы во время исполнения. Ядро фабрики. Ключ, как мы уже говорили, имеет тип, задаваемый первым параметром шаблона — всё просто. А вот значение — как раз тот волшебный указатель на шаблонную функцию. После специализации (любым классом, унаследованным от Base) функция имеет одинаковую сигнатуру, то есть всегда (при любом параметре шаблона) принимает известное и одинаковое количество и тип аргументов, и всегда возвращает значение одного известного типа — указатель на объект типа Base. То есть задача решена! :).
- Четырнадцатая строчка: функция add добавляет в std::map classes пару id типа ID (первый параметр шаблона класса) и адрес статичной специализированной шаблонной функции-члена instantiator. Уже говорили.
- Семнадцатая (ну захотелось мне поупражняться в написании числительных) строчка: функция get возвращает указатель на instantiator нужного класса. Обратите внимание, что в данный момент объект не создаётся, это пользователь делает потом сам с помощью выданного instantiator-а. Это то, чего я и хотел добиться.
Я уже говорил, что я новичок в C++? :) Так вот, вполне возможно, что я кое-где наврал (а может, только кое-где не наврал...), но точно могу сказать, что это всё работает.
Просто возьмите последний листинг, воткните его вместо #include «factory.h» и проверьте получившееся в вашем любимом компиляторе, не забыв включить поддержку стандарта C++11.
P. S. Вообще, изначально я хотел оставить мой вариант фабрики комментарием к первой статье, но зачем-то написал пост… в любом случае, буду крайне рад обсуждению, ошибкам в ЛС и… сильно не бейте. :)
P. P. S. Ах, только хотел отправить, но вспомнил, что есть же ещё Ideone! Залил туда пример, можно любоваться. :)
Автор: dbanet