Search code examples
c++template-meta-programming

Is there a way to gather explicit specializations of a templated function into a container at compile time or startup?


I am trying to setup a system where I or a user can implement a specialization to a templated function and have it be put into a static collection of function pointers defined in another file, or even another static library, ideally at compile time, or at startup. I would like the addition of an implementation to this container to not require manual intervention in another file but be mostly handled by the compiler or at least before any instance of the derived class exists.

Here is an approximation of what I am trying to do:

#include <map>
#include <typeindex>
#include <type_traits>
#include <vector>

//this would actually live in another library
class Base{};

//the templated function that would be specialized in other files
template<typename T, typename = std::enable_if_t<std::is_base_of_v<Base,T>>>
void Func(Base* arg){
  }

//should contain function pointer to all implemented Func specialization
std::map<std::type_index,void(*)(Base*)> funcs = {};

//the type index matches the derived type of Base
void Process(std::multimap<std::type_index,Base*> payload) {
    for (auto&[type_index, obj] : payload) {
        if (funcs.contains(type_index))
            funcs[type_index](obj);
    }

}

Any pointers on coming up with a macro to registered specialized version of Func in that map ? Or is there a metaprogramming trick using templates and constexpr that could reach a similar result? An explanation on why this would not be possible is also welcome.

For context I am trying to make an editor for a game engine and I would like to have functions that describe how to render the inspectors through ImGui all contained in that map. My goal is being able to easily create new functions for custom component types by adding and modifying a single file.

I have found patterns online that could allow me to build a container of template<unsigned int N>func() function pointers if the indexes are sequential, which does not apply here. And I'm not sure how to enforcethat this would be evaluated after all specialization are found, if it is even possible to have the specialization defined in other files.

I have considered using a visitor pattern, but I would need to keep adding to the visitor class every time I create a new component type.

I know that I cannot automatically gather all specializations via a constexpr call, but I could imagine a macro to use around all specializations to automate the process(even if it means wrapping all of them in templated class, which I will likely do).


Solution

  • You have to rely on a "self-registering" static object in each translation unit that defines the specialized Func<T>.

    Instead of putting:

    std::map<std::type_index,void(*)(Base*)> funcs = {};
    

    Directly at the namespace scope, it's better to hide it behind an accessor function:

    Registry.h:

    #pragma once
    
    #include <typeindex>
    #include <map>
    #include "Base.h"
    
    using FuncType = void(*)(Base*);
    
    inline std::map<std::type_index, FuncType>& getRegistry() {
        static std::map<std::type_index, FuncType> registry;
        return registry;
    }
    

    Also, declare your primary template:

    Registry.h:

    template<typename T, typename = std::enable_if_t<std::is_base_of_v<Base, T>>>
    void Func(Base* arg) {
        // Default or fallback implementation, if desired
    }
    

    We want a small function or a template that takes a derived type T and inserts its corresponding Func into the registry:

    template<typename T>
    bool registerType() {
        getRegistry()[typeid(T)] = &Func<T>;
        return true;
    }
    

    In each .cpp file or library where you implement a custom inspector for a particular derived type Derived, you do something like this:

    #include "Registry.h"
    
    template <>
    void Func<Derived>(Base* arg) {
    }
    
    namespace {
        static const bool registered_Derived = registerType<Derived>();
    }