I'm building a low-latency system, in which I have functions with more than one enum value as template parameters.
Sometimes I need to resolve the enum values at runtime, but I don't want to check for them everywhere; instead, just check once, and then pass it as a template argument for the rest of the functions. My actual example is distracting, so here is an MRE:
#include <iostream>
enum class Color {
Red,
Green,
Blue
};
enum class Type {
Car,
Bus,
Truck
};
struct Vehicle {
Color color;
Type type;
};
template <typename Func>
constexpr auto perform(Vehicle& vehicle, Func&& func) {
// Ideally, we'd want something like this:
// constexpr auto color = [&] {
// if (vehicle.color == Color::Red) return Color::Red;
// if (vehicle.color == Color::Green) return Color::Green;
// return Color::Blue;
// }();
//
// constexpr auto type = [&] {
// if (vehicle.type == Type::Car) return Type::Car;
// if (vehicle.type == Type::Bus) return Type::Bus;
// return Type::Truck;
// }();
//
// return func.template operator()<color, type>();
if (vehicle.color == Color::Red) {
if (vehicle.type == Type::Car) {
return func.template operator()<Color::Red, Type::Car>();
} else if (vehicle.type == Type::Bus) {
return func.template operator()<Color::Red, Type::Bus>();
} else {
return func.template operator()<Color::Red, Type::Truck>();
}
} else if (vehicle.color == Color::Green) {
if (vehicle.type == Type::Car) {
return func.template operator()<Color::Green, Type::Car>();
} else if (vehicle.type == Type::Bus) {
return func.template operator()<Color::Green, Type::Bus>();
} else {
return func.template operator()<Color::Green, Type::Truck>();
}
}
if (vehicle.type == Type::Car) {
return func.template operator()<Color::Blue, Type::Car>();
} else if (vehicle.type == Type::Bus) {
return func.template operator()<Color::Blue, Type::Bus>();
}
return func.template operator()<Color::Blue, Type::Truck>();
}
int main() {
Vehicle v { Color::Red, Type::Car };
auto func = [&] <Color color, Type type> {
if constexpr (color == Color::Red) {
std::cout << "Red ";
} else if constexpr (color == Color::Green) {
std::cout << "Green ";
} else {
std::cout << "Blue ";
}
if constexpr (type == Type::Car) {
std::cout << "Car";
} else if constexpr (type == Type::Bus) {
std::cout << "Bus";
} else {
std::cout << "Truck";
}
};
perform(v, func);
}
How can I write the perform
function in a way similar to the commented code?
This solution (also) relies on std::variant
and std::integral_constant
(I was writing the answer and didn't see the other answer based on the same arguments but this one is a little bit more generic).
Here, a Selector
class manages the "runtime to compile time" selection, e.g. one could write things like:
Selector<ColorArr>{v.color}
where ColorArr
is a std::array
made of all values of Color
. This selector assigns the runtime value v.color
to a std::variant
made of std::integral_constant
types, one per enum value.
With this Selector
, one can write:
int main()
{
Vehicle v { Color::Blue, Type::Bus, Dummy::Bar };
auto func = [&] <Color color, Type type, Dummy dummy> {
if constexpr (color == Color::Red) { std::cout << "Red "; }
else if constexpr (color == Color::Green) { std::cout << "Green "; }
else { std::cout << "Blue "; }
if constexpr (type == Type::Car) { std::cout << "Car"; }
else if constexpr (type == Type::Bus) { std::cout << "Bus"; }
else { std::cout << "Truck"; }
};
// Now, we can use the std::visit with as many enum values as we want.
// Each value is encapsulated by a Selector instance
std::visit([&](auto...args) {
func.template operator()<args...> ();
}, Selector<ColorArr>{v.color}(), Selector<TypeArr>{v.type}(), Selector<DummyArr>{v.dummy}());
}
where adding a new enum (like Dummy
) becomes quite easy.
Now, here is the Selector
class; it takes as non type template argument an array arr
(c++20
required) made of all values of an enumeration.
template<auto arr, std::size_t Nmax=arr.size()>
class Selector
{
public:
// We get the type of array items
using value_type = typename decltype(arr)::value_type;
// We build a std::variant whose types are integral constants made of each values in the array
using alltypes = decltype ([] <std::size_t...Is> (std::index_sequence<Is...>) {
return std::variant <std::integral_constant<value_type,arr[Is]>...> {};
} (std::make_index_sequence<Nmax>{}));
// The constructor takes an enum value as entry and set the std::variant
Selector (value_type val) : value (select<Nmax>(val)) {}
// Return the std::variant
auto operator() () const { return value; }
private:
template<int N=Nmax, int Pending=0>
constexpr auto select (value_type val) {
if constexpr (N > 0) {
if (val==arr[Pending]) {
return alltypes {std::integral_constant<value_type,arr[Pending]>{}};
}
else {
return select <N-1,Pending+1> (val);
}
}
else { return alltypes{}; }
}
alltypes value;
};
The drawback of this approach is that one needs to define a std::array
holding the values of each enum:
constexpr std::array ColorArr = { Color::Red, Color::Green, Color::Blue };
constexpr std::array TypeArr = { Type::Car, Type::Bus, Type::Truck };
constexpr std::array DummyArr = { Dummy::Foo, Dummy::Bar };
Update
I just removed the useless n
parameter in select
function (redundant with the template parameter Pending
)