c++templatesc++20c++-conceptsc++23

C++20 and newer - what's the best way to implement an "enum with more functionality"?


I've seen scores of threads over the years on how you could wrap an enum or template a class with the enum to add addtional functionality.

Consider the use case. I have an enum. I want to be able to create an object of a class around the enum class so that I can

A simple way to do this is


enum class color : uint16_t {
unknown = 0,
red,
green,
blue,
};

class colorClass {
public:
colorClass(){ val = color::unknown; }
colorClass(color v)
 :val(v)
{}

//copy, move, assignment ctors section

bool isRed(){ return val == color::red; }
// more such functions

std::string toString() {
using enum color;
switch (val) {
  case red : return "red";
  case blue: return "blue";
  case green: return "green";
  case unknown: [[fallthrough]];
  default: return "unknown";
}
}

private:
 color val;
};

This works just fine. Now if I want to restrict this class some more I can use templating and concepts

enum class color : uint16_t {
unknown = 0
red,
green,
blue,
};


template<typename E>
concept ValidColor = std::is_enum_v<E>;

template<ValidColor E>
class colorClass {
// Pretty much the same implementation?
}; 

My memory's a little fuzzy but I think I've seen some clever implementations for toString using underlying type. I'm not looking to use any 3rd party libs.

Do I have the "modern" way right? Apart from enforcing the usage of the enum does the addition of templating provide any benefits?

Edit: I'm not asking just about reflection (and thanks for all the answers in that direction) but how C++20 and C++23 can enhance this "enum with a few tricks" class


Solution

  • Despite the fact that the year is 2024 and I am using C++20, I still find myself using X macros for this as I have yet to find a better (non-external-library) solution:

    #define DEFINE_ENUM_VAL(class_name, name) name,
    #define DEFINE_ENUM_STR_CONVERSION(class_name, name) case class_name::name: return #name;
    
    #define FOR_EACH_FOOBAR(DO) \
        DO(FooBar, ONE) \
        DO(FooBar, TWO) \
        DO(FooBar, THREE)
    
    enum class FooBar
    {
        FOR_EACH_FOOBAR(DEFINE_ENUM_VAL)
    };
    
    inline auto to_string(FooBar val) -> std::string
    {
        switch (val) {
            FOR_EACH_FOOBAR(DEFINE_ENUM_STR_CONVERSION)
        }
        std::unreachable();
    }
    
    #undef FOR_EACH_FOOBAR
    
    #undef DEFINE_ENUM_STR_CONVERSION
    #undef DEFINE_ENUM_VAL
    

    This is fairly easy to extend for any behaviors that can be derived from the enum type name and value identifiers. However, a lot of the surrounding boilerplate will have to be repeated for every enum type that you want to support (though I bet this could be genericized a bit further if you wanted to avoid that, at the cost of relying even more on preprocessor magic).