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