Search code examples
c++c++17factory

C++ Library & Self registering classes: Factory map empty in client application


Let there be a C++ library (let's call it lib) which gets included as a static library in an application (let's call it app). Within the lib there's a base class node. Each subclass of a node is identified by a UUID. I employ a self registering pattern to ensure that new classes register themselves at the factory. The factory allows to build a node subclass object based on the provided UUID. The app builds objects through the lib's factory::build() function.

My factory is based on the code of this brilliant blog post.

I've adapted this code to use a UUID (using boost.uuid) instead of a string as all the classes being created need a UUID assigned by them anyway (for external dependency reasons).

The problem I am hitting is that if I do not "manually" create an object instance of each node_template subclass (i.e. A and B), the factory::m_generators map is empty. This is of course due to the fact that the various node-subclasses were never instantiated and therefore never registered themselves.

Here's my runnable minimum example (live demo at coliru):

#include <iostream>
#include <unordered_map>
#include <functional>
#include <memory>
#include <boost/functional/hash.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/string_generator.hpp>

class node;

/**
 * @brief The factory class to build @p node items.
 */
class factory
{
public:
    using key_type  = boost::uuids::uuid;
    using key_hash  = boost::hash<key_type>;
    using generator = std::function<std::unique_ptr<node>()>;

    template<typename Derived>
    struct registrar
    {
        registrar(const key_type& key)
        {
            factory::instance().register_generator(key, [](){
                return std::make_unique<Derived>();
            });
        }

        registrar(const std::string& uuid_string)
        {
            try {
                boost::uuids::string_generator gen;
                registrar(gen(uuid_string));
            } catch (...) {
                ;
            }
        }
    };

    static factory& instance() noexcept
    {
        static factory f;
        return f;
    }

    bool register_generator(const key_type& key, generator&& generator)
    {
        auto [it, emplaced] = m_generators.try_emplace(key, std::move(generator));
        return emplaced;
    }

    [[nodiscard]] std::unique_ptr<node> build(const key_type& key) const
    {
        if (const auto& it = m_generators.find(key); it not_eq m_generators.cend())
            return it->second();
        return nullptr;
    }

    [[nodiscard]] std::unique_ptr<node> build(const char* uuid_string) const noexcept
    {
        try {
            boost::uuids::string_generator gen;
            return build(gen(uuid_string));
        } catch (...) {
            return nullptr;
        }
    }

private:
    std::unordered_map<key_type, generator, key_hash> m_generators;

    factory() = default;
    factory(const factory& other) = default;
    factory(factory&& other) = default;
    virtual ~factory() = default;
};


/**
 * @brief The node base class.
 */
struct node
{
    node(const std::string& uuid_string) :
        m_uuid_string(uuid_string)
    {
    }

    [[nodiscard]] const std::string& uuid_string() const noexcept {
        return m_uuid_string;
    }

private:
    std::string m_uuid_string;
};

/**
 * @brief A template for @p node subclasses.
 */
template <class derived>
struct node_template :
    node,
    factory::registrar<derived>
{
    node_template(const std::string& uuid_string) :
        node(uuid_string),
        factory::registrar<derived>(uuid_string)
    {
    }
};

struct A : node_template<A> {
    A() : node_template("63cb8eeb-b90b-46c7-aaa8-3a349fcba3c5") { }
};

struct B : node_template<B> {
    B() : node_template("1f24abfc-936f-4524-ae3b-cc346335ecbb") { }
};

static void build_and_print(const std::string& uuid_string)
{
    if (auto node = factory::instance().build(uuid_string.c_str()); node)
        std::cout << "node.uuid_string() = " << node->uuid_string() << std::endl;
    else
        std::cout << "Cannot build node object: Unknown UUID." << std::endl;
}

int main(void)
{
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    /// PROBLEM: If I do not construct these objects, they never register themselves at the factory. ///
    ////////////////////////////////////////////////////////////////////////////////////////////////////
#if 1
    A a;
    B b;
#endif

    // A
    build_and_print("63cb8eeb-b90b-46c7-aaa8-3a349fcba3c5");

    // B
    build_and_print("1f24abfc-936f-4524-ae3b-cc346335ecbb");

    // Unknown UUID
    build_and_print("9b20cc29-c7ca-4796-acb2-6ca6b80fa934");

    return 0;
}

As long as I keep the object instantiations in line 136 and 137 I am able to build further objects through the factory. But once I remove then (eg. change line 135 to #if 0) the factory generators map is empty.

I think that I understand the problem as in that the class never gets to register itself as there's never an object being constructed. However, I am not sure how to fix the problem.

Currently there's a very ugly header file in lib which gets included by app. The header creates a dummy object of each class. This is of course anything but nice and also kind of defeats the entire purpose of a self registering class.

What am I missing here?


Solution

  • From the blog post, you are missing the static member registered (also called "// The really fun part"). Having and instantiating such a static variable in the base class forces it to be instantiated in all derived classes and this will register the class as a side effect.

    EDIT: There is another very small but very important piece of code in the blog post:

        Registrar() : Base(Key{}) { (void)registered; }
    

    This will ensure that registered is used. Because a static variable is only instantiated the first time it is used, otherwise the function is not called.

    In your case, adding the following to node_template should work:

    template <class derived>
    struct node_template :
        node,
        factory::registrar<derived>
    {
        node_template(const std::string& uuid_string) :
            node(uuid_string),
            factory::registrar<derived>(uuid_string)
        {
            (void) registered;
        }
    
        static bool do_register() {
            derived d; // I am not sure if one should in some way force this to not be optimized away.
            return true;
        }
    
        inline static bool registered = do_register();
    };