2

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>() {}
maluyazi
  • 21
  • 3
  • I don't know why the difference, but if you create at least one instance of your classes, they will behave identically. – sklott Feb 02 '23 at 05:55
  • Yes, that is true, the behavior of MyFactory1 makes sense, I just wonder if this is the standard of C++ or if it is a feature of the compiler I run this code in Compiler Explorer to see the assembly code. Both gcc and clang compilers initialize Registrable::isRegistered – maluyazi Feb 02 '23 at 07:14
  • I'll be honest, reading up on static member variables is giving me headaches. What I do know: don't write this code, it's stupidly complicated. – Passer By Feb 02 '23 at 07:55
  • Consider adding [language-lawyer] tag. – sklott Feb 02 '23 at 07:58

1 Answers1

1

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.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • Thanks for correct the std::cout part. An Explansion here "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." I did not expect that way... Whereas the opposite. What I wonder is how MyFactory2 causes the initialize.. – maluyazi Feb 02 '23 at 09:25