Reputation: 5597
I have multiple classes that share a common base class, like this:
class Base {};
class DerivedA : public Base {};
class DerivedB : public Base {};
class DerivedC : public Base {};
Now, I need to know which of these derived classes to instantiate during runtime (based on input). For example, if input is "DerivedA"
, I need to create a DerivedA
object. The input is not necessarily a string, it could be an integer as well - the point is that there is a key of some sort and I need a value to match the key.
The problem is, though, how do I instantiate the class? C++ does not have built-in reflection like C# or Java. A commonly suggested solution I've found is to use a factory method like this:
Base* create(const std::string& name) {
if(name == "DerivedA") return new DerivedA();
if(name == "DerivedB") return new DerivedB();
if(name == "DerivedC") return new DerivedC();
}
This would be sufficient if there's only a couple of classes, but becomes cumbersome and probably slow if there's tens or hundreds of derived classes. I could quite easily automate the map creation process to produce a std::map<std::string, ***>
, but I have no idea what to store as the value. AFAIK, pointers to constructors are not allowed. Again, if I do a factory using this map, I'd still need to write a factory method for each type, making it even more cumbersome than the example above.
What would be an efficient way to handle this problem, especially when there's lots of derived classes?
Upvotes: 4
Views: 611
Reputation: 20780
You can always store std::function<Base*()>
as you always return pointers to Base
from your create
function:
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;
}
As Angew suggested, you should return std::unique_ptr
instead of raw pointers. If the user of create
function wants a raw pointer or a std::shared_ptr
he/she can just "grab" the raw pointer and use it.
UPDATE:
Next method provides a convenient semi-automatic way of registering new types without changing old code.
I don't recommend using it because it depends on the linker (the moment of creating global variables might be delayed), they way you compile the code(executable, static library, dynamic library), it allocates memory before main()
starts and it creates weird named global variables.
Use it only if you really know what you are doing and know on what platforms you are using the code!
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);
Upvotes: 8
Reputation: 36630
I could quite easily automate the map creation process to produce a std::map, but I have no idea what to store as the value.
You need to store a factory method as the value, e.g. a static method which creates an instance of your class:
class Base {};
class DerivedA : public Base {
public:
static Base* create();
...
}
...
Base* DerivedA::create() {
return new DerivedA();
}
You can then implement the name/lookup through a map like
typedef Base* (*FACTORY_FUNCTION)();
std::map<std::string, FACTORY_FUNCTION> factories;
...
factories["ClassA"] = ClassA::create;
if I do a factory using this map, I'd still need to write a factory method for each type
Since these factory methods are very simple, you can automate their creation by a simple code generation tool (e.g. with a simple shell script). You can either maintain a list of classes, or retrieve this list from your header files (e.g. by grep
ping for the class
keyword and retrieve the succeeding class name, or even better by using some analysis tool which properly parses the header files).
With that information, you can automatically create the necessary code to automatically add the factory methods to each class. With the same approach, you could also generate the registration function which needs to be called once, so that your objects are getting registered.
Upvotes: 1
Reputation: 171107
One way to solve this is to use the design pattern Prototype.
Basically, you wouldn't create the derived class objects by direct initialisation, but by cloning a prototype instead. Your create()
function is actually a realisation of the Factory method design pattern. You can use Prototype inside the implementation, like this:
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();
}
Error checking left out of the example for brevity.
In a real project, you should of course use a smart pointer (such as std::unique_ptr
) instead of raw pointers to manage the objects' lifetimes.
Upvotes: 3