c++templateslambdaconstexpr

How can I manage perform n + m checks instead of n * m for constexpr values?


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?


Solution

  • 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 };
    

    Demo


    Update

    I just removed the useless n parameter in select function (redundant with the template parameter Pending)