\$\begingroup\$

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

"; } }; // 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

"; } }; 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.