Шаблонная фабрика объектов (ещё раз, и в пятнадцать строк)

в 3:42, , рубрики: c++, Программирование, фабрика объектов, шаблоны

Привет!

Я очень новичок в 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), благо никакого труда дописать это не составляет. Но а вообще я ещё считаю, что код примера должен быть максимально прост и очевиден, без этих, безусловно, неизбежных дьявольских деталей: так легче понимать, а именно это сейчас и нужно.

  1. #include <map>
  2.  
  3. template<class ID,class Base,class ... Args> class GenericObjectFactory{
  4. private:
  5.     typedef Base* (*fInstantiator)(Args ...);
  6.     template<class Derived> static Base* instantiator(Args ... args){
  7.         return new Derived(args ...);
  8.     }
  9.     std::map<ID,fInstantiator> classes;
  10.  
  11. public:
  12.     GenericObjectFactory(){}
  13.     template<class Derived> void add(ID id){
  14.         classes[id]=&instantiator<Derived>;
  15.     }
  16.     fInstantiator get(ID id){
  17.         return classes[id];
  18.     }
  19.  
  20. };
  21.  

И всё! Разве не эленгантно? Мне кажется, я превзошёл решение от 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js