2
\$\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\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.

\$\endgroup\$
3
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Mast
    Commented Nov 2, 2018 at 16:50
  • \$\begingroup\$ @Mast I agree with you on the compilation fix for clang. But I would argue that adding comments and making the example more clear does not invalidate feedback from other users and helps to decrease the time it takes future reviewers to give useful feedback. \$\endgroup\$
    – HenrikS
    Commented Nov 2, 2018 at 17:12
  • \$\begingroup\$ In this case? Yes. So feel free to add those comments again, as long as they don't invalidate the current answer. \$\endgroup\$
    – Mast
    Commented Nov 2, 2018 at 18:02

1 Answer 1

3
\$\begingroup\$

Your code compile on GCC but not on Clang, he don't like (as me) these few lines :

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()};

In both register_class(), you use the operator new, but you never delete. Keep in mind that they come in pair. Or you can (and should) doing like in the blog post you followed, using std::make_unique.

Using integer as identifier don't make sense for the final user.

Playing with template, code can be quickly unreadable. Try to space and indents long template chain.

\$\endgroup\$
2
  • \$\begingroup\$ First of all thank you for your feedback. Seems like clang didn't like s_registered to be const. I don't know why but I fixed it. Yes, I use new in the factory functions. registry_t::make() is only responsible for creating the objects. The user has to manage the destruction by himself. Additionally the raw ptr is actually converted into unique_ptr, see registry_t::make. Using integers as identifier can make lot's of sense. For example when parsing a binary format. The whole point of the class is also to be able to use any kind of key. I just used unsigned as an example. \$\endgroup\$
    – HenrikS
    Commented Nov 2, 2018 at 16:49
  • \$\begingroup\$ I added some comments to explain parts of the code better and improved the example. Please have look. \$\endgroup\$
    – HenrikS
    Commented Nov 2, 2018 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.