c++templatesc++17metaprogrammingtemplate-meta-programming

C++ Templates vs OOD woes - How can I get around implementing something like 'templated class member variables'?


Im trying to save a lot of code duplication using templates. Ideally I would like to do something along the lines of this (I know its garbage code):

class Registries {
public:

    template<typename T>
    void AddToRegistry(T thing) {
        registry.push_back(thing);
    }

    template<typename T>
    std::vector<T>* GetRegistry<T>() {
        return &registry();
    }

private:
    template<typename T>
    std::vector<T> registry; // auto-magically created vectors for each used type
};


int main() {
    Registries registries;

    CoolThing thing1;
    NiceThing thing2;
    // ... Many N types

    registries.AddToRegistry(thing1);
    registries.AddToRegistry(thing2);

    std::vector<CoolThing>* cool_registry = registries.GetRegistry<CoolThing>();
    std::vector<NiceThing>* nice_registry = registries.GetRegistry<NiceThing>();
}

It of course doesnt work because I can't template a class member - a variable won't be created for each type, and these functions wouldnt know what one to return):

I could do this, but they'll all be stored in static memory (not ideal):

class CoolThing {
public:
    static std::vector<CoolThing> registry;
};

class NiceThing {
public:
    static std::vector<NiceThing> registry;
};

class Registries {
public:

    template<typename T>
    void AddToRegistry(T thing) {
        T::registry.push_back(thing);
    }
}


int main() {
    Registries registries;

    registries.AddToRegistry(CoolThing());
    registries.AddToRegistry(NiceThing());
    // ... Many N types
}

I also tried switch and if-else within a template but they seem incompatible:

class CoolThing {
public:
    const static ThingType TYPE = COOL;
};

class NiceThing {
public:
    const static ThingType TYPE = NICE;
};

class Registries {
public:

    template<typename T>
    void AddToRegistry(T thing) {

        switch (T::TYPE) {
        
        case ThingType::COOL:
            cool_registry.push_back(thing);
            return;

        case ThingType::NICE:
            nice_registry.push_back(thing);
            return;
        
        default:
            return;
        }
    }

    template<typename T>
    std::vector<T>* GetRegistry<T>() {
        
        if (T::TYPE == ThingType::COOL) {

            return &cool_registry;
        } 
        else if (T::TYPE == ThingType::NICE) {

            return &nice_registry;
        }

        return nullptr;
    }

private:
    std::vector<CoolThing> cool_registry;
    std::vector<NiceThing> nice_registry;
    // ... Many N types
};


int main() {
    Registries registries;

    CoolThing thing1;
    NiceThing thing2;
    // ... Many N types

    registries.AddToRegistry(thing1);
    registries.AddToRegistry(thing2);

    std::vector<CoolThing>* = registries.GetRegistry<CoolThing>();
    std::vector<NiceThing>* = registries.GetRegistry<NiceThing>();
}

I feel like my options are the static way, or writing a long list of (20+ types) function overloads to add to the correct registry type.

Any template whizzes out there know an alternative? Many thanks


Solution

  • Another solution that does not rely on runtime type erasure (std::any) or global singletons would be to store a tuple of std::vector<T>:

    #include <vector>
    
    template <typename... Ts>
    class Registries {
    public:
    
        template<typename T>
        void AddToRegistry(T thing) {
            GetRegistry<T>().push_back(thing);
        }
    
        template<typename T>
        std::vector<T>& GetRegistry() {
            return std::get<std::vector<T>>(registry);
        }
    
    private:
        std::tuple<std::vector<Ts>...> registry;
    };
    
    struct CoolThing {};
    struct NiceThing {};
    
    
    int main() {
        Registries<CoolThing, NiceThing> registries;
    
        CoolThing thing1;
        NiceThing thing2;
        // ... Many N types
    
        registries.AddToRegistry(thing1);
        registries.AddToRegistry(thing2);
    
        std::vector<CoolThing>& cool_registry = registries.GetRegistry<CoolThing>();
        std::vector<NiceThing>& nice_registry = registries.GetRegistry<NiceThing>();
    }
    

    Note that this requires you to know all possible types when instantiating the registry.

    Working demo

    PS: If that bothers you, here is a solution with type erasure that does not have this restriction: https://godbolt.org/z/nvWchfMaK