Home > Enterprise >  Initialization of static template member with CRTP
Initialization of static template member with CRTP

Time:02-02

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.

  • Related