Search code examples
c++templatesswitch-statementc++17

Generate switch cases for each type in a tuple/parameter pack


I am trying to figure out how to use template "magic" to automatically generate a switch statement for each type in a tuple. Here is my example code:

#include <string>

enum class AttributeID
{
    ATTRIBUTE1,
    ATTRIBUTE2,
    ATTRIBUTE3
};

struct ATTRIBUTE1
{
    static constexpr auto kID = AttributeID::ATTRIBUTE1;
    using Value = unsigned int;
    
    Value v;
};

struct ATTRIBUTE2
{
    static constexpr auto kID = AttributeID::ATTRIBUTE2;
    using Value = char;
    
    Value v;
};

struct ATTRIBUTE3
{
    static constexpr auto kID = AttributeID::ATTRIBUTE3;
    using Value = std::string;
    
    Value v;
};

class Attributes
{
public:
    using ID = AttributeID;
    
    template <typename T>
    auto& get()
    {
        return std::get<T>(impl_).v;
    }
    
private:
    std::tuple<ATTRIBUTE1, ATTRIBUTE2, ATTRIBUTE3> impl_;
};

template <typename Attribute>
void foodleAttribute(Attribute& attr, unsigned int data)
{
    //...
    //This template will be specialized based upon the type of Attribute
}

void fiddleAttribute(Attributes& attrs, Attributes::ID id, unsigned int data)
{
    switch(id)
    {
    case ATTRIBUTE1::kID:
        foodleAttribute(attrs.get<ATTRIBUTE1>(), data);
        break;
    case ATTRIBUTE2::kID:
        foodleAttribute(attrs.get<ATTRIBUTE2>(), data);
        break;
    case ATTRIBUTE3::kID:
        foodleAttribute(attrs.get<ATTRIBUTE3>(), data);
        break;
    }
}

I would like to rewrite the fiddleAttribute function as such

template <typename Attributes>
void fiddleAttribute(Attributes& attrs, typename Attributes::ID id, unsigned int data)
{
    //Generate a switch case (or equivalent) for each type (its kID constant) in the Attributes tuple
}

So that it generates something equivalent to the explicit switch statement I wrote (switch on the id, call the templated foodleAttribute function).


Solution

  • The first option is to use a std::index_sequence with a fold expression. (Credits to @TedLyngmo for actually including kID in the solution and relaxing the restriction that the ids should be continuous)

    template <std::size_t... Is>
    void fiddleAttributeImpl(Attributes& attrs, Attributes::ID id, unsigned int data, std::index_sequence<Is...>)
    {
        void(((id == std::tuple_element_t<Is, Types>::kID
                && (foodleAttribute(attrs.get<std::tuple_element_t<Is, Types>>(), data), false)) || ...));
    }
    
    void fiddleAttribute(Attributes& attrs, Attributes::ID id, unsigned int data)
    {
        fiddleAttributeImpl(attrs, id, data, std::make_index_sequence<std::tuple_size_v<Types>>{});
    }
    

    The expression to be folded is

    id == std::tuple_element_t<Is, Types>::kID 
                && (foodleAttribute(attrs.get<std::tuple_element_t<Is, Types>>(), data), true)
    

    The part after && is only executed if the runtime id equals the corresponding kID. The comma operator makes the subexpression evaluate to true and short circuits to avoid further comparsion. We use std::tuple_element_t to obtain the Isth type from the type list.

    See Compiler Explorer: https://godbolt.org/z/xaYd8a8r6


    Alternatively, an approach that is more similar in nature to a switch statement is to build an array of function pointers from lambdas, which is then indexed at run time. In the following implementation the ids are assumed to be continuous and start from 0. I am not entirely sure whether this restriction can be lifted without building an additional map.

    Here we use the unary plus operator trick to convert the non-capturing lambdas to raw function pointers, which is more efficient than using e.g. std::functions. We also make use of CTAD so that we don't need to manually specify the template arguments for std::array.

    template <std::size_t... Is>
    void fiddleAttributeImpl(Attributes& attrs, Attributes::ID id, unsigned int data, std::index_sequence<Is...>)
    {
        static constexpr std::array
        jumpTable{
            +[](Attributes& attrs, unsigned int data){foodleAttribute(attrs.get<std::tuple_element_t<Is, Types>>(), data);}
            ...
        };
        
        jumpTable[static_cast<int>(id)](attrs, data);
    }
    
    

    See compiler explorer: https://godbolt.org/z/Wc6TnrEnv