I've made registry/factory class for C++ so I can instantiate different classes at runtime based on some kind of key. My design is partially based on this blog post: http://www.nirfriedman.com/2018/04/29/unforgettable-factory/ . So you might read it first to get a broad overview. In short: Instead of manually adding derived classes to a big switch statement in some factory function, your base class only needs to inherit from registry_t<my_base_class_t, my_key_type_t>
. Registrars just need to inherit from my_base_class_t::register_const<my_derived_class_t, my_identifier>
or my_base_class_t::register_dyn<my_derived_class_t>
. All registration is done automatically without any additional code. It supports passing parameters to the constructors and it's possible to change the returned ptr type (default: unique_ptr
).
#include <memory>
#include <unordered_map>
template<typename t, t value>
struct dummy_user_t {};
// t_derived: class inheriting from registry_t
// t_key: the type that should be passed as key
// t_ptr: the type of ptr that should be returned to the user
// t_args: constructor signature
template<typename t_derived, typename t_key, typename t_ptr = std::unique_ptr<t_derived>, typename... t_args>
struct registry_t
{
using factory_type = t_derived*(*)(t_args&&... args);
friend t_derived;
private:
registry_t() = default;
struct shared_t
{
template<typename t, auto>
friend struct register_const;
friend registry_t;
private:
// avoid undefined static member initialization order
static std::unordered_map<t_key, factory_type>& get_factories()
{
static std::unordered_map<t_key, factory_type> s_factories;
return s_factories;
}
};
protected:
using identifier_t = t_key;
public:
template<typename t, auto>
friend struct register_const;
[[nodiscard]]
static t_ptr make(const t_key& key, t_args&&... args)
{
static_assert(std::is_base_of_v<registry_t<t_derived, t_key, t_ptr, t_args...>, t_derived>,
"Trying to instantiate derived class of non-registry");
// return instantiated object as requested ptr type (default std::unique_ptr)
return t_ptr {shared_t::get_factories().at(key)(std::forward<t_args>(args)...)};
}
public:
template<typename t_registrar, auto key>
struct register_const : t_derived
{
friend t_registrar;
private:
using t_derived::t_derived;
static const bool s_registered;
// "use" s_registered so it is actually instantiated
using value_user_t = dummy_user_t<const bool&, s_registered>;
struct private_t
{
friend register_const;
private:
static bool register_class() // associate factory function with corresponding key
{
shared_t::get_factories()[t_key {key}] = [](t_args&&... args) -> t_derived*
{
return new t_registrar(std::forward<t_args>(args)...);
};
return true;
}
};
};
template<typename t_registrar>
struct register_dyn : t_derived
{
friend t_registrar;
private:
using t_derived::t_derived;
static const bool s_registered;
using value_user_t = dummy_user_t<const bool&, s_registered>;
struct private_t
{
friend register_dyn;
private:
static bool register_class()
{
shared_t::get_factories()[t_registrar::get_key()] = [](t_args&&... args) -> t_derived*
{
return new t_registrar(std::forward<t_args>(args)...);
};
return true;
}
};
};
};
// initialize s_registered with register_class() so it's called at program startup
template<typename t_derived, typename t_key, typename t_ptr, typename... t_args>
template<typename t_registrar, auto key>
const bool registry_t<t_derived, t_key, t_ptr, t_args...>::register_const<t_registrar, key>::s_registered
{registry_t<t_derived, t_key, t_ptr, t_args...>::register_const<t_registrar, key>::private_t::register_class()};
template<typename t_derived, typename t_key, typename t_ptr, typename... t_args>
template<typename t_registrar>
const bool registry_t<t_derived, t_key, t_ptr, t_args...>::register_dyn<t_registrar>::s_registered
{registry_t<t_derived, t_key, t_ptr, t_args...>::register_dyn<t_registrar>::private_t::register_class()};
And here's a small example:
#include <iostream>
// paste implementation here
enum class animal_type
{
pig = 1,
cow
};
// make animal_t a registry with key type unsigned and no constructor arguments:
struct animal_t : registry_t<animal_t, animal_type>
{
virtual ~animal_t() = default;
virtual void print_name() const = 0;
};
// pig_t inherits from animal_t and is assigned the key 1
struct pig_t : animal_t::register_const<pig_t, animal_type::pig>
{
void print_name() const override
{
std::cout << "pig\n";
}
};
// cow_t also inherits from animal_t but the key can be determined at runtime
struct cow_t : animal_t::register_dyn<cow_t>
{
static identifier_t get_key()
{
return animal_type::cow;
}
void print_name() const override
{
std::cout << "cow\n";
}
};
int main()
{
// create a pig_t
std::unique_ptr<animal_t> x {animal_t::make(animal_type::pig)};
// create a cow_t
auto y {animal_t::make(animal_type::cow)};
// correct objects have been returned
x->print_name();
y->print_name();
return 0;
}
Since this is quite a big chunk of code here's already a small FAQ for questions that are very likely to be asked:
Why make everything private and declare t_derived
as friend? Why not use protected?
registry_t
relies on the CRT Pattern. The private constructor guarantees that only the correct class can inherit from it:
struct my_class : registry_t<my_other_class, int> {}; // <- Error
What are shared_t
and private_t
for?
The ultimate drawback of the trick mentioned above is that all private members bubble up to the inheriting class. Putting these in nested classes prevents that.