Auto registration of types with C++ ODR use

147 Views Asked by At

I'm trying to get a demo code working to show auto registration with the help of ODR use (I've only learned about this in the last day so forgive me if I'm not using that correctly).

This is the interesting bits of the code

template <const bool*>
struct OdrUse {};

bool Register(const char *) {return true;}

template <typename T>
struct Registered {
  static bool registredInInit;  // If 'const' is added, clang does not accept the code as well.
  static constexpr OdrUse<&registredInInit> odrUser{};
};

template<typename T>  // If defined after 'Class', both gcc and clang accept the code.
bool Registered<T>::registredInInit = Register(T::NAME);

struct Class : Registered<Class> {
    static constexpr const char* NAME = "ZIP";
};

int main() {
}

Demo

Lot of the code has been copied from https://www.cppstories.com/2018/02/factory-selfregister/

I'm getting this error when compiling

<source>: In instantiation of 'bool Registered<Class>::registredInInit':
<source>:9:27:   required from 'struct Registered<Class>'
<source>:15:16:   required from here
<source>:13:51: error: incomplete type 'Class' used in nested name specifier
   13 | bool Registered<T>::registredInInit = Register(T::NAME);
      |                                                   ^~~~
Compiler returned: 1

Looking at the error, why does the compiler need RegisteredInFactory<ZipCompression> fully defined to access kFactoryName ?

1

There are 1 best solutions below

0
Oersted On

The fix seems to merely be (based on original question):

#include <iostream>
#include <map>
#include <memory>
#include <string>

// Interface for the class.
class ICompressionMethod {
   public:
    ICompressionMethod() = default;
    virtual ~ICompressionMethod() = default;

    virtual void Compress() = 0;
};

// Helper class holding static methods for factory
class CompressionMethodFactory {
   public:
    using TCreateMethod = std::unique_ptr<ICompressionMethod> (*)();

   public:
    CompressionMethodFactory() = delete;

    static bool Register(const std::string name, TCreateMethod createFunc) {
        if (auto it = s_methods.find(name); it == s_methods.end()) {
            s_methods[name] = createFunc;
            std::cout << name << " registered\n";
            return true;
        }
        return false;
    }

    static std::unique_ptr<ICompressionMethod> Create(const std::string& name) {
        if (auto it = s_methods.find(name); it != s_methods.end())
            return it->second();
        return nullptr;
    }

   private:
    static std::map<std::string, TCreateMethod> s_methods;
};
std::map<std::string, CompressionMethodFactory::TCreateMethod>
    CompressionMethodFactory::s_methods;

template <bool*>
struct OdrUse {};

template <typename T>
class RegisteredInFactory {
   protected:
    static bool s_bRegistered;
    static constexpr OdrUse<&s_bRegistered> odr{};
};

class ZipCompression : public ICompressionMethod,
                       public RegisteredInFactory<ZipCompression> {
   public:
    static constexpr const char* kFactoryName = "ZIP";
    void Compress() override {
        // if (s_bRegistered) std::cout << "registered\n";
        std::cout << "in compress\n";
    }

    static std::unique_ptr<ICompressionMethod> CreateMethod() {
        return std::make_unique<ZipCompression>();
    }
    static std::string GetFactoryName() { return "ZIP"; }
};

template <typename T>
bool RegisteredInFactory<T>::s_bRegistered =
    (CompressionMethodFactory::Register(T::kFactoryName, T::CreateMethod),
     true);

int main(int argc, char* argv[]) {
    std::cout << "main starts...\n";
    auto pMethod = CompressionMethodFactory::Create("ZIP");
    if (pMethod != nullptr) {
        pMethod->Compress();
    } else {
        std::cout << "factory creation failed\n";
    }
    std::cout << "end"
              << "\n";
    return 0;
}

Live

[EDIT: version with new code snippet]:

template <const bool*>
struct OdrUse {};

bool Register(const char *) {return true;}

template <typename T>
struct Registered {
  static bool registredInInit;  // If 'const' is added, clang does not accept the code as well.
  static constexpr OdrUse<&registredInInit> odrUser{};
};


struct Class : Registered<Class> {
    static constexpr const char* NAME = "ZIP";
};

template<typename T>  // If defined after 'Class', both gcc and clang accept the code.
bool Registered<T>::registredInInit = Register(T::NAME);

int main() {
}

Live

registredInInit has to be initialized after declaring Class so that gcc see Class members. But I'm failing to see why it would be a requirement? I'm acknowledging that a more detailed answer is still required. It might be easier by trying to reproduce the issue with a smaller example. Maybe some answer there: c++ template code order parsing/CRTP An hypothesis:

class Class: public Registered<Class>

At this point Class is only declared, and is still an incomplete type as gcc rightly says. It also triggers an instanciation of Registered<Class> which, in turn, requires initialization of its static members, which calls Register(NAME) but Class is still incomplete at this time and NAME unknown. Thus gcc would be right, clang and msvc wrong (which I found strange). In general CRTP is working when you are calling base member function from the inherited class, after its definition. So these functions are instantiated only after the inherited class being a complete class. Am I wrong? NB With this snippet, it is not anymore a classical CRTP as the inherited class is not a template but I think that the explanations still hold.

[EDIT]

Another way to fix is to let Class trigger the init. Here is this solution implemented in @yeputons example, which is more readable:

template <const bool*>
struct OdrUse {};

void Register(const char*) {}

template <typename T>
struct Registered {
    static bool registredInInit;  // If 'const' is added, clang does not accept
                                  // the code as well.
    static constexpr OdrUse<&registredInInit> odrUser{};
};

struct Class : Registered<Class> {
    static constexpr const char* NAME = "ZIP";
    static bool LocalRegister;
    static bool Register() {
        // call global Register
        Registered<Class>::registredInInit = (::Register(NAME), true);
        return true;
    }
};
bool Class::LocalRegister = Class::Register();

int main() {}

Live Demo

When LocalRegister is initialized, Class is a complete type, the call to Class::Register();, triggers the call to ::Register(NAME) where NAME is properly defined.