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).
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 id
s 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 Is
th 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 id
s 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::function
s. 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