c++switch-statementcompile-timeenum-class

product enum class in c++


Is there a way to automatically (and at compile time) define a "product enum class" (i.e. an enum class that takes on values in the product space between two given enum classes)?

So if I have

enum class Color { RED, GREEN };
enum class Shape { CIRCLE, TRIANGLE };

then I want my compiler to make me a type

enum class ComboResult { RED_CIRCLE, RED_TRIANGLE, GREEN_CIRCLE, GREEN_TRIANGLE };

without me having to type all the possibilities myself.

The motivation would be like a switch statement that can exhaustively search every element in the product space.


Solution

  • Since primary motive is switch-case statements, I make 2 assumptions:

    1. Number of enumerators is small

    2. Compile-time evaluation is needed

    With that in mind:

    #include <type_traits>
    #include <concepts>
    #include <ranges>
    #include <functional>
    #include <stdexecpt>
    
    template<typename enm>
    concept reflectable_enum =
        std::is_enum_v<enm>
    and requires(enm ev) {  
        {std::to_underlying(ev)}
        ->    std::unsigned_integral;
        {max_enum_value(ev)}
        ->    std::same_as<enm>;
    };
    
    template<reflectable_enum ... es>
    constexpr auto cartesian_enum_product(es const ... evs)
    { 
        using int_t = std::common_type_t<std::underlying_type_t<es>...>;
        std::array values {static_cast<int_t>(evs)...};
        constexpr auto static cum_cs = [](auto evs){
            std::array maxcs
            { 1
            , static_cast<int_t>
                (max_enum_value(evs))
            ...};
            for( auto&& [a,b]
               : maxcs 
               | std::views::adjacent<2>){
                 b*=a;
                 if((~(int_t{})/b)<a)
                     throw std::logic_error
                           {"large enums"};
            };
            return maxcs;
        }(es{}...);
        auto const result = std::ranges::fold_left(
             std::views::zip_transform
                ( std::multiplies<>{}
                , values, cum_cs )
           , int_t{}
           , std::plus<>{} )
        );
        enum class result_t : int_t{};
        return static_cast<result_t>(result);
    };
    

    The rnumerations need some restriction and metadata for the task. I restricted them to unsigned enums for simpler illustration; otherwise a min_enum_value function would be involved too. Since the function is declared constexpr, and definition can actually produce compile time constants, it can be used in core constant expression like case lables:

    enum X:unsigned{x0, xm};
    enum Y:unsigned{y0, ym};
    
    //Meta data:
    consteval max_enum_value(X){return xm;};
    consteval max_enum_value(Y){return ym;};
    
    constexpr auto x0_y0 //for SO syntax highlighter
        = cartesian_enum_product(X::x0, Y::y0);
    
    switch(cartesian_enum_product(x,y)){
    default:
        return default_result(x,y);
    case x0_y0:
        return result_x0_y0();
    };
    

    In the above snippet, I had to define the case value outside the switch, to get the syntax highlighter to work, but it should eork with direct function call too.

    I tried to avoid C++26 reflection, because it's too new and needs more study.

    Edit: the original simple product was wrong. Now it's more complex.