Создание класса по имени класса

У меня есть несколько классов, которые имеют общий базовый класс, например:

class Base {};

class DerivedA : public Base {};
class DerivedB : public Base {};
class DerivedC : public Base {};

Теперь мне нужно знать, экземпляры каких из этих производных классов создавать во время выполнения (на основе ввода). Например, если ввод "DerivedA", мне нужно создать объект DerivedA. Входные данные не обязательно являются строкой, это также может быть целое число - дело в том, что есть какой-то ключ, и мне нужно значение, соответствующее ключу.

Однако проблема в том, как мне создать экземпляр класса? C++ не имеет встроенного отражения, как C# или Java. Обычно предлагаемое решение, которое я нашел, - использовать фабричный метод следующим образом:

Base* create(const std::string& name) {
    if(name == "DerivedA") return new DerivedA();
    if(name == "DerivedB") return new DerivedB();
    if(name == "DerivedC") return new DerivedC();
}

Этого было бы достаточно, если бы было всего несколько классов, но он стал бы громоздким и, вероятно, медленным, если бы были десятки или сотни производных классов. Я мог бы довольно легко автоматизировать процесс создания карты, чтобы создать std::map<std::string, ***>, но я понятия не имею, что хранить в качестве значения. Насколько мне известно, указатели на конструкторы не допускаются. Опять же, если я создам фабрику, используя эту карту, мне все равно нужно будет написать фабричный метод для каждого типа, что сделает его еще более громоздким, чем в приведенном выше примере.

Что было бы эффективным способом решения этой проблемы, особенно при наличии большого количества производных классов?


person manabreak    schedule 24.09.2014    source источник
comment
Похоже, что классический фабричный шаблон - это то, что вам нужно здесь.   -  person Fantastic Mr Fox    schedule 24.09.2014
comment
@ Бен, я не уверен, как это может решить проблему поиска. В конце концов, вам нужно сопоставить некоторый код со строками.   -  person juanchopanza    schedule 24.09.2014
comment
Хорошим чтением по этому вопросу является Современный дизайн C++, универсальное программирование и дизайн. Применение шаблонов, Глава 8. Фабрики объектов   -  person Andreas Fester    schedule 24.09.2014
comment
@juanchopanza factory pattern - это часть решения - вам нужен какой-то (статический) метод/функция, которая может создать объект определенного класса. Вторая часть - это сопоставление имени с фабричным методом, которое можно легко реализовать с помощью карты, которая имеет имя в качестве ключа и указатель на фабричный метод в качестве значения. Третья часть — это механизм регистрации — каждый класс должен зарегистрировать свой фабричный метод на карте, используя уникальное имя (идентификатор типа). Я думаю, что OP уже знает это в целом, но ищет решение, когда есть много классов   -  person Andreas Fester    schedule 24.09.2014
comment
@Andreas Андреас На самом деле это не часть решения. Это было бы реализовать решение, но оно не решает фундаментальную проблему. Для простого сопоставления, подобного показанному в примере OP, это не принесет вам много пользы. Решение может быть таким же простым, как unordered_map<string, function<Base*(const string&)>>.   -  person juanchopanza    schedule 24.09.2014
comment
Проблема может заключаться в вашем дизайне, если вам нужно создать экземпляр другого класса, каждый раз получая другое перечисление/что угодно... и у вас есть большое количество вариантов   -  person Marco A.    schedule 24.09.2014
comment
@МаркоА. Я действительно не знаю, как это сделать по-другому - разные классы (и другое поведение) выбираются на основе ввода (файла).   -  person manabreak    schedule 24.09.2014


Ответы (3)


Вы всегда можете сохранить std::function<Base*()>, поскольку вы всегда возвращаете указатели на Base из вашей функции create:

class Base {};

class DerivedA : public Base {};
class DerivedB : public Base {};
class DerivedC : public Base {};

Base* create(const std::string& type)
{
    static std::map<std::string, std::function<Base*()>> type_creator_map =
    {
        {"DerivedA", [](){return new DerivedA();}},
        {"DerivedB", [](){return new DerivedB();}},
        {"DerivedC", [](){return new DerivedC();}}
    };

    auto it = type_creator_map.find(type);
    if(it != type_creator_map.end())
    {
        return it->second();
    }

    return nullptr;
}

Как предложил Энгью, вы должны возвращать std::unique_ptr вместо необработанных указателей. Если пользователю функции create нужен необработанный указатель или std::shared_ptr, он/она может просто "схватить" необработанный указатель и использовать его.

ОБНОВИТЬ:

Следующий метод обеспечивает удобный полуавтоматический способ регистрации новых типов без изменения старого кода.

Я не рекомендую его использовать, потому что это зависит от компоновщика (момент создания глобальных переменных может быть отложен), от того, как вы компилируете код (исполняемый файл, статическая библиотека, динамическая библиотека), он выделяет память до запуска main() и создает глобальные переменные со странными именами.

Используйте его только в том случае, если вы действительно знаете, что делаете, и знаете, на каких платформах вы используете код!

class Base {};

std::map<std::string, std::function<Base*()>>& get_type_creator_map()
{
    static std::map<std::string, std::function<Base*()>> type_creator_map;
    return type_creator_map;
}

template<typename T>
struct RegisterTypeHelper
{
    RegisterTypeHelper(const std::string& id)
    {
        get_type_creator_map()[id] = [](){return new T();};
    }
};

Base* create(const std::string& type)
{
    auto& type_creator_map = get_type_creator_map();
    auto it = type_creator_map.find(type);
    if(it != type_creator_map.end())
    {
        return it->second();
    }

    return nullptr;
}

#define RegisterType(Type) static RegisterTypeHelper<Type> register_type_global_##Type(#Type)

class DerivedA : public Base {};
RegisterType(DerivedA);

class DerivedB : public Base {};
RegisterType(DerivedB);

class DerivedC : public Base {};
RegisterType(DerivedC);
person Mircea Ispas    schedule 24.09.2014
comment
Это именно то, что я искал - я знал, что смогу как-то подойти к этому, используя лямбда-выражения! Конечно, это не происходит автоматически, но, как предложил Андреас, я могу написать небольшой инструмент для разбора заголовков и создания карты с использованием проанализированных имен классов. Спасибо! - person manabreak; 24.09.2014
comment
Это может быть (полу) автоматическим в зависимости от компоновщика и если вы принимаете использование некоторых глобальных/синглетонов. Я обновлю ответ коротким образцом - person Mircea Ispas; 24.09.2014
comment
Замыкающие возвращаемые типы не нужны. std::function принимает все, чей тип возвращаемого значения может быть неявно преобразован в тип возвращаемого значения, указанный в параметре шаблона. - person T.C.; 24.09.2014
comment
@Т.С. Я этого не знал. Спасибо! Я отредактировал свой ответ. - person Mircea Ispas; 24.09.2014
comment
Обратите внимание, что второе решение, хотя и очень понятное, страдает от фиаско порядка инициализации. Если вы определяете register_type_global_xxx в одном T.U. а вы пытаетесь создать объект того же типа из другого Т.У. перед вызовом main вы все равно можете получить nullptr. - person sbabbi; 24.09.2014
comment
@sbabbi Да, я пытался уточнить, что это очень опасно и не рекомендуется. Насколько я знаю, стандарт требует, чтобы глобальные переменные были созданы до того, как любая функция из того же T.U. называется. На всех платформах, которые я пробовал, глобальные переменные создаются одновременно, но это не является переносимым, гарантированным или безопасным :) - person Mircea Ispas; 24.09.2014

Один из способов решить эту проблему — использовать шаблон проектирования Prototype.

По сути, вы не будете создавать объекты производного класса путем прямой инициализации, а вместо этого клонируете прототип. Ваша функция create() на самом деле является реализацией шаблона проектирования Factory method. Вы можете использовать Prototype внутри реализации, например:

class Base
{
public:
  virtual ~Base() {}
  virtual Base* clone() = 0;
};

class DerivedA : public Base
{
public:
  virtual DerivedA* clone() override { return new DerivedA; }
};


Base* create(const std::string &name)
{
  static std::map<std::string, Base*> prototypes {
    { "DerivedA", new DerivedA },
    { "DerivedB", new DerivedB },
    { "DerivedC", new DerivedC }
  };
  return prototypes[name]->clone();
}

Проверка ошибок исключена из примера для краткости.

В реальном проекте вы, конечно, должны использовать интеллектуальный указатель (например, std::unique_ptr) вместо необработанных указателей для управления временем жизни объектов.

person Angew is no longer proud of SO    schedule 24.09.2014
comment
В чем преимущество перед решением OP? Вам все еще нужно сопоставить строку имени класса с самим классом. - person MatthiasB; 24.09.2014
comment
@MatthiasB Цитируя OP: я мог бы легко автоматизировать процесс создания карты для создания std::map<std::string, ***>, но я понятия не имею, что хранить в качестве значения. (выделено мной). Я показываю, что хранить в качестве значения и как его использовать. - person Angew is no longer proud of SO; 24.09.2014
comment
Но я не вижу необходимости в прототипах. Кроме того, вам нужно сделать так, чтобы объекты newed в prototypes были deleted. - person juanchopanza; 24.09.2014
comment
@juanchopanza Что бы вы тогда хранили вместо прототипов? Что касается delete, то я добавил комментарий об умных указателях. - person Angew is no longer proud of SO; 24.09.2014
comment
Я бы хранил функции, которые возвращают (умный) указатель на объект. - person juanchopanza; 24.09.2014

Я мог бы легко автоматизировать процесс создания карты для получения std::map, но понятия не имею, что хранить в качестве значения.

Вам нужно сохранить фабричный метод в качестве значения, например. статический метод, который создает экземпляр вашего класса:

class Base {};

class DerivedA : public Base {
public:
    static Base* create();

    ...
}

...

Base* DerivedA::create() {
    return new DerivedA();
}

Затем вы можете реализовать имя/поиск через карту, например

typedef Base* (*FACTORY_FUNCTION)();
std::map<std::string, FACTORY_FUNCTION> factories;

...

factories["ClassA"] = ClassA::create;

если я создам фабрику, используя эту карту, мне все равно нужно будет написать фабричный метод для каждого типа

Поскольку эти фабричные методы очень просты, вы можете автоматизировать их создание с помощью простого инструмента генерации кода (например, с помощью простого сценария оболочки). Вы можете либо сохранить список классов, либо получить этот список из ваших заголовочных файлов (например, с помощью grepping для ключевого слова class и получить последующее имя класса, или, что еще лучше, с помощью какого-либо инструмента анализа, который правильно анализирует заголовочные файлы).

С помощью этой информации вы можете автоматически создать необходимый код для автоматического добавления фабричных методов в каждый класс. Используя тот же подход, вы также можете сгенерировать функцию регистрации, которую нужно вызвать один раз, чтобы ваши объекты были зарегистрированы.

person Andreas Fester    schedule 24.09.2014
comment
Я не голосовал против и не вижу причин для этого. Просто комментарий: почему классы должны содержать статический метод для создания экземпляров? Это кажется ненужным ограничением, которое делает решение менее общим (т. е. оно будет работать только для классов с этим статическим членом). - person juanchopanza; 24.09.2014
comment
@juanchopanza Итак, ваш вопрос заключается в том, почему фабричный метод является статическим членом класса - он с таким же успехом может быть общедоступным методом вне класса или статическим членом внутри отдельного класса, который предоставляет все необходимые заводские функции, верно? - person Andreas Fester; 24.09.2014
comment
Я больше думал о любом вызываемом объекте, который не принимает параметров и возвращает (умный) указатель. Это может быть функция, функтор, лямбда, что угодно. - person juanchopanza; 24.09.2014
comment
@juanchopanza Правильно - я думаю, что тогда, когда я реализовал описанный выше подход, идея заключалась в том, чтобы просто инкапсулировать фабрику в класс (по сути, класс должен знать, как создавать себя) ... - person Andreas Fester; 24.09.2014