Search code examples
c++templates

How to define template function signature based on enum class parameter?


Is it possible to declare a template function such that the type of the arguments of the function depend on an enum class template parameter?

The use case is roughly the following. The application maintains a collection of entities of different classes, and mappings between those entities. The mappings are encoded as struct members as follows:

#include<unordered_set>
using std::unordered_set;

struct Entity1;
struct Entity2;

struct Entity {
};

struct Entity1 : public Entity {
   Entity1* m_parent; // source of first mapping
   Entity2* m_owner; // source of first mapping
   unordered_set<Entity1*> m_children; // destination of first mapping
};

struct Entity2 : public Entity {
   unordered_set<Entity1*> m_elements; // destination of second mapping
};

To code some generic procedures on mappings, I would like to be able to identify the type of the source and destination of the mapping.

enum class Mapping { M1, M2 };

// the following is illegal
template <Mapping r> Sig {
    typename Dom;
    typename Ran;
};

using Sig.Dom<Mapping::M1> = Entity1;
using Sig.Ran<Mapping::M1> = Entity1;
using Sig.Dom<Mapping::M2> = Entity1;
using Sig.Ran<Mapping::M2> = Entity2;

Here is an example of generic procedure I would like to be able to instantiate.

template <Mapping r>
void link(Sig.Dom<r>* obj1, Sig.Ran<r>* obj2);

template<>
void link<Mapping::M1>(Entity1* obj1, Entity1* obj2) {
    if (obj1->m_parent != nullptr) {
        obj1->m_parent->m_children.erase(obj1);
    }
    obj1.m_parent = obj2;
    if (obj1->m_parent != nullptr) {
        obj1->m_parent->m_children.insert(obj1);
    }
}

template<>
void link<Mapping::M2>(Entity1* obj1, Entity2* obj2) {
    if (obj1->m_owner != nullptr) {
        obj1->m_owner->m_elements.erase(obj1);
    }
    obj1.m_owner = obj2;
    if (obj1->m_owner != nullptr) {
        obj1->m_owner->m_elements.insert(obj1);
    }
}

Is there a way to achieve this in C++?


Solution

  • Here you go:

    template <Mapping r>
    struct LinkParameters
    {};
    
    template<>
    struct LinkParameters<Mapping::M1>
    {
        using First = Entity1*;
        using Second = Entity1*;
    };
    template<>
    struct LinkParameters<Mapping::M2>
    {
        using First = Entity1*;
        using Second = Entity2*;
    };
    
    template <Mapping r>
    void link(typename LinkParameters<r>::First obj1, typename LinkParameters<r>::Second obj2)
    {
        if constexpr (r == Mapping::M1) {
            if (obj1->m_parent != nullptr) {
                obj1->m_parent->m_children.erase(obj1);
            }
            obj1.m_parent = obj2;
            if (obj1->m_parent != nullptr) {
                obj1->m_parent->m_children.insert(obj1);
            }
        } else {
            if (obj1->m_owner != nullptr) {
                obj1->m_owner->m_elements.erase(obj1);
            }
            obj1.m_owner = obj2;
            if (obj1->m_owner != nullptr) {
                obj1->m_owner->m_elements.insert(obj1);
            }
        }
    }
    

    Full program demo

    The idea is to have as much explicit specialisation of the trait LinkParameters as there are possible specialisations of link.

    For the definition of link(), I decided to go with the constexpr if solution to demonstrate another approach. You only can decide what's the best, all are mostly compatible so experiment and choose.

    As an alternative, link could be a callable type defined with the usual idiom (global definition + explicit specialisations). As it would be a type, it could include or nest the LinkParameters trait for more encapsulation. This solution is left as an exercice.