I was trying an auto-registering CRTP factory class just like crtp-registering. But I've got a curious problem here with static template member initialization. Here is the test code:
#include <string>
#include <unordered_map>
#include <iostream>
#include <memory>
#include <functional>
#include <string_view>
template <typename Base>
class Factory
{
template <typename, typename>
friend class Registrable;
private:
static std::unordered_map<std::string, std::shared_ptr<Base>> &map()
{
static std::unordered_map<std::string, std::shared_ptr<Base>> map;
return map;
}
template <typename Derived>
static void subscribe(std::string name)
{
// insert already-exist-check here
std::cout << "registered: " << name << std::endl;
map().emplace(std::move(name), std::static_pointer_cast<Base>(std::make_shared<Derived>(Derived())));
}
};
template <typename T>
constexpr auto type_name() noexcept
{
std::string_view name, prefix, suffix;
#ifdef __clang__
name = __PRETTY_FUNCTION__;
prefix = "auto type_name() [T = ";
suffix = "]";
#elif defined(__GNUC__)
name = __PRETTY_FUNCTION__;
prefix = "constexpr auto type_name() [with T = ";
suffix = "]";
#endif
name.remove_prefix(prefix.size());
name.remove_suffix(suffix.size());
return name;
}
template <typename Base, typename Derived>
class Registrable
{
protected:
Registrable()
{
isRegistered = true;
}
~Registrable() = default;
static bool init()
{
Factory<Base>::template subscribe<Derived>(std::string(type_name<Derived>()));
return true;
}
private:
static bool isRegistered;
};
template <typename Base, typename Derived>
bool Registrable<Base, Derived>::isRegistered = Registrable<Base, Derived>::init();
struct MyFactoryBase
{
virtual ~MyFactoryBase() = default;
virtual void method() const = 0;
};
struct MyFactory1 : public MyFactoryBase, public Registrable<MyFactoryBase, MyFactory1>
{
void method() const override { std::cout << "yay Class1" << std::endl; }
MyFactory1() = default;
};
struct MyFactory2 : public MyFactoryBase, public Registrable<MyFactoryBase, MyFactory2>
{
void method() const override { std::cout << "yay Class1" << std::endl; }
MyFactory2() : MyFactoryBase(), Registrable<MyFactoryBase, MyFactory2>() {}
};
int main()
{
return 0;
}
my gcc version : gcc (GCC) 8.3.1 20191121 (Red Hat 8.3.1-5)
the code's output is :
registered: MyFactory2
why MyFactory2 can auto reigst while MyFactory1 cannot, what's the difference between the default constructor and the almost-default constructor
MyFactory2() : MyFactoryBase(), Registrable<MyFactoryBase, MyFactory2>() {}
CodePudding user response:
TL;DR The code has undefined behaviour (surprise!). Don't write this code.
The undefined behaviour results from a little accident due to your demonstration. Non-inline variables with static storage duration that is a specialization, in your case isRegistrated
, has unordered dynamic initialization. Since its initialization is necessarily dynamic, it has unordered initialization. std::cout
is only guaranteed to be initialized before ordered initialization, so you are using an uninitialized std::cout
, which is UB.
Suppose we modify your snippet so we avoid std::cout
in the initialization.
template <typename Base>
struct Factory
{
static auto& list()
{
static std::vector<std::string> v;
return v;
}
template <typename Derived>
static void subscribe(std::string name)
{
list().push_back(std::move(name));
}
};
What you expect is each specialization of isRegistered
is initialized because we implicitly instantiated Registrable
, which in turn causes subscribe
to be called. That is not the case.
Each MyFactory
is a class, whose definition causes the implicit instantiation of its base class template Registrable
. The implicit instantiation of a class template instantiates the declarations but not the definitions of its member functions and member variables. Importantly, isRegistered
will not be defined just because we inherited from Registrable
, thus its initialization won't ever happen.
MyFactory2
defines a default constructor, which calls and instantiates Registrable()
. Registrable()
in turn uses and instantiates isRegistered
. Therefore you see MyFactory2
being registered.
MyFactory1
declares a explicitly-defaulted constructor within the class. Such a declaration is called a non-user-provided defaulted function, which is only defined after its odr-use or when needed in constant evaluation. To force MyFactory1
to be registered, you simply need to use its constructor somewhere.
But we're still not done yet. Even if isRegistered
is instantiated, it is not guaranteed to be initialized. Since isRegistered
is dynamically initialized, it may be deferred until just before the first odr-use of any static or thread-local variable from the same translation unit that is not part of initialization. Given that your main
is empty, it is possible that no initialization happens regardless.