多年来,我已经看到过大量关于如何包装枚举或使用枚举模板化类以添加附加功能的帖子。

考虑一下用例。我有一个枚举。我希望能够围绕枚举类创建一个类的对象,这样我就可以

  • 转换为字符串
  • 返回值具体信息

一个简单的方法是


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

这很好用。现在,如果我想进一步限制此类,我可以使用模板和概念

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

我的记忆有些模糊,但我想我见过一些使用底层类型的 toString 的巧妙实现。我不想使用任何第三方库。

我的方法对吗?除了强制使用枚举外,添加模板还能带来其他好处吗?

编辑:我问的不仅仅是关于反射(感谢大家在这方面的回答),而是 C++20 和 C++23 如何增强这个“使用一些技巧的枚举”类

7

  • 2
    没有办法让编译器将枚举名称映射到字符串。有一些反射库可以帮助您做到这一点。也许 C++26 会获得反射,并且可以本地完成此操作。


    – 

  • 1
    如果您的新代码无法编译,您应该包含一个以及完整的编译器错误消息。


    – 

  • 1
    您可以使用X Macros分配枚举名称和字符串而无需重复它们,但这当然不是一种现代的方式 – 它即使在纯 C 中也能工作。


    – 


  • 在您的示例中,所有方法colorClass都是私有的,并且toString方法不采用零参数。


    – 

  • 1
    从 github 下载 Magic Enum。问题解决了。(除非你的问题是可以想象到的每个符合要求的编译器的绝对可移植性)。


    – 


最佳答案
2

尽管现在是 2024 年并且我正在使用 C++20,但我仍然发现自己在使用,因为我还没有找到更好的(非外部库)解决方案:

#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

对于任何可以从枚举类型名称和值标识符派生的行为,这都相当容易扩展。但是,对于您想要支持的每种枚举类型,许多相关的样板代码都必须重复(不过我敢打赌,如果您想避免这种情况,可以进一步将其通用化,但代价是更加依赖预处理器魔法)。

1

  • 1
    特别是,除了使用预处理器之外,如果没有反射,就没有办法避免(冗长且容易出错的)重复从标识符生成字符串的名称。


    – 

0x8000对于最大枚举值为左右的情况( 的 ICE 限制),有一个可行的技巧std::make_index_sequence。然后,您可以使用填充元素的数组将字符串映射到值std::source_location。然后,您需要从平台相关的名称修改中修剪字符串:

template<typename enm>
requires std::is_enum_v<enm>
constexpr std::string_view enum_raw_name()
{return std::source_location::current().function_name();};

constexpr std::string_view enum_demangle_trim(std::string_view);//has platform specific implementation 

template<typename enm>
requires std::is_enum_v<enm>
constexpr std::pair enum_bounds{ 
    std::to_underlying(enm{}),
    std::numeric_limits<std::underlying_t<enm>>::max()
};

template<typename enm>
requires std::is_enum_v<enm>
constexpr std::string_view enum_value_name(enm const e, std::underlying_type_t<enm> u = {}) {
     using int_t = decltype(u);
     constexpr auto bounds  = enum_bounds<enm>;
     auto constexpr range = std::min(0x8000uz,std::size_t{bounds.second}-bounds.first);
     static auto constexpr map = []<std::size_t ... i>(std::index_sequence<i ...>){
            return std::array{ 
                   enum_demangle_trim(enum_raw_name<static_cast<enm>(int_t{i} + bounds.first)>())
            ..., "out of range"};
     }(std::make_index_sequence<range>{});
     return map[min(std::size_t{int_t{e}}-bounds.first, range)];
};

上述实现的限制是枚举类型的最大值和负枚举值 – 这被视为超出范围。元编程也可以启用负值;变量模板enum_bounds需要明确专门化。

另一个痛点是依赖于平台的实现enum_demangle_trim——它应该从两侧缩小底层字符串来检索枚举名称。