确定C++枚举类的元素数量是否可能?

121

是否有可能确定 C++ enum class 的基数:

enum class Example { A, B, C, D, E };

我尝试使用sizeof,但它返回的是一个枚举元素的大小。

sizeof(Example); // Returns 4 (on my architecture)

有没有标准的方法来获取基数(例如我的例子中的5)?


1
我认为可能有一个特定的C++11机制。 - bquenin
11
顺便提一下,这不是重复的问题。enumenum class是非常不同的概念。 - Shoe
@Shoe…但是它们真的是这样吗? - Kyle Strand
1
这似乎是一个 XY 问题,我知道这是很久以前的事了,但你还记得为什么需要这样做吗?你不能迭代枚举类的值,所以知道数字有什么好处呢? - Fantastic Mr Fox
6
也许不是楼主的原因,但一个例子是将一个数组初始化为枚举类型的“大小”,然后使用枚举器来索引该数组。我猜还有很多其他的例子。 - Ad N
18个回答

104

没有直接的方法,但您可以使用以下技巧:

enum class Example { A, B, C, D, E, Count };

然后可以使用static_cast<int>(Example::Count)获得枚举元素的基数。

当然,这仅在您允许自动分配枚举值(从0开始)时有效。如果不是这种情况,则可以手动将正确的基数分配给Count,这与维护一个单独的常量没有什么区别:

enum class Example { A = 1, B = 2, C = 4, D = 8, E = 16, Count = 5 };

唯一的劣势是编译器会允许您将 Example::Count 用作枚举值的参数,因此如果您使用它,请小心!(尽管我个人认为在实践中这不是一个问题。)


1
枚举类中的枚举值是类型安全的,因此在此处,“Count”将是Example类型而不是int类型,对吗?您需要先将“Count”转换为int才能将其用于大小。 - Man of One Way
@Man:是的,使用enum class而不是普通的enum会使这个技巧变得有些混乱。我会编辑并添加一个强制转换以便更清晰明了。 - Cameron
27
如果您在此枚举中使用 switch 语句,任何一个合理的编译器都会警告您缺少一个 case。如果经常使用,这可能非常令人烦恼。在这种特定情况下,单独使用一个变量可能更好。 - Fantastic Mr Fox
@FantasticMrFox 我完全同意,这是基于经验的。编译器警告也非常重要。我已经发布了一种替代方法,更符合您推荐的精神。 - arr_sea

45

对于C++17,您可以使用来自库https://github.com/Neargye/magic_enummagic_enum::enum_count

magic_enum::enum_count<Example>() -> 4.

哪里有缺陷?

该库使用编译器特定的技巧(基于__PRETTY_FUNCTION__ / __FUNCSIG__),它适用于Clang >= 5、MSVC >= 15.3和GCC >= 9。

我们遍历所给区间范围,并找到所有具有名称的枚举,这将是它们的计数。 阅读更多关于限制的信息。

在此帖子https://taylorconor.com/blog/enum-reflection中了解更多关于这种技巧的信息。


2
这太棒了!无需修改现有代码即可计算枚举成员的数量。此外,这似乎实现得非常优雅(只是浏览了一下代码)! - andreee
2
一个巨大的限制是枚举值被限制在小范围内(最多可以扩展到short范围,而不是int范围)。如果库失败了,它不会以static_assert或编译时错误的方式失败,它只会“剪辑”枚举值并返回您枚举值的子集。虽然在C++中可以做得更好,但这是一个重要的限制。仍然投了赞成票,你的库是我最喜欢的,在我能使用它的情况下。 - NoSenseEtAl
编译时间的影响将会很有趣,特别是在更大的范围内。 - Trass3r
@Trass3r 我正在开发新版本,完成后我会尝试测量编译时间的影响。 - Neargye
1
可能存在优化潜力。不幸的是,上次我尝试使用clang的-ftime-trace来报告constexpr代码时,并没有报告太多信息:https://github.com/llvm/llvm-project/issues/42754 - Trass3r

33
// clang-format off
constexpr auto TEST_START_LINE = __LINE__;
enum class TEST { // Subtract extra lines from TEST_SIZE if an entry takes more than one 
    ONE = 7
  , TWO = 6
  , THREE = 9
};
constexpr auto TEST_SIZE = __LINE__ - TEST_START_LINE - 3;
// clang-format on

这段内容源自于UglyCoder的回答,但是在以下三个方面进行了改进:

  • type_safe枚举中没有额外的元素(BEGINSIZE)(Cameron的回答也存在此问题)。
    • 编译器不会抱怨它们在switch语句中缺失(这是一个重要问题)
    • 它们不会无意中传递给需要你的枚举类型的函数(不是常见的问题)
  • 它不需要强制转换即可使用(Cameron的回答也存在此问题)。
  • 减法不会影响枚举类类型的大小。

它保留了UglyCoder的优点,即枚举器可以被分配任意值,而Cameron的回答则没有。

一个问题(与UglyCoder共享,但与Cameron不共享)是它使换行和注释具有重要意义...这是出乎意料的。因此,有人可能会添加带有空格或注释的条目,而不调整TEST_SIZE的计算。这意味着代码格式化程序可能会破坏此功能。在evg656e's的评论之后,我编辑了答案以禁用clang-format,但如果您使用不同的格式化程序,则需要自行注意。

3
像 clang-format 这样的工具也很容易破坏这个。 - evg656e
1
感谢 @evg656e。我为 clang-format 添加了一个解决方法,并明确提到代码格式化器作为添加空格问题的一部分。 - Eponymous

9
// clang-format off
enum class TEST
{
    BEGIN = __LINE__
    , ONE
    , TWO
    , NUMBER = __LINE__ - BEGIN - 1
};
// clang-format on

auto const TEST_SIZE = TEST::NUMBER;

// or this might be better 
constexpr int COUNTER(int val, int )
{
  return val;
}

constexpr int E_START{__COUNTER__};
enum class E
{
    ONE = COUNTER(90, __COUNTER__)  , TWO = COUNTER(1990, __COUNTER__)
};
template<typename T>
constexpr T E_SIZE = __COUNTER__ - E_START - 1;

聪明!当然不能有任何注释或不寻常的间距,对于非常大的源文件,底层值类型可能比其他情况下更大。 - Kyle Strand
@Kyle Strand:有一个问题就是如果使用char并且你有超过256个枚举项,编译器会通知你关于截断等方面的信息。__LINE__是一个整数文字,使用#line的限制为[1,2147483647]。 - UglyCoder
好的,即便是原本应该是 short 的枚举类型,在进行 Unity 构建时也可能会升级到 int。 (我认为这更多是 Unity 构建的问题,而不是您提出的技巧的问题。) - Kyle Strand
诀窍? :-) 我会用它,但很少并且审慎。就像编码中的所有内容一样,我们需要权衡利弊,特别是长期维护的影响。 我最近使用它从C #define列表(OpenGL wglExt.h)创建枚举类。 - UglyCoder

8

您可以通过使用std :: initializer_list的技巧来解决:

#define TypedEnum(Name, Type, ...)                                \
struct Name {                                                     \
    enum : Type{                                                  \
        __VA_ARGS__                                               \
    };                                                            \
    static inline const size_t count = []{                        \
        static Type __VA_ARGS__; return std::size({__VA_ARGS__}); \
    }();                                                          \
};

使用方法:

#define Enum(Name, ...) TypedEnum(Name, int, _VA_ARGS_)
Enum(FakeEnum, A = 1, B = 0, C)

int main()
{
    std::cout << FakeEnum::A     << std::endl
              << FakeEnun::count << std::endl;
}

6

有一种基于X()宏的技巧:图像,您具有以下枚举:

enum MyEnum {BOX, RECT};

将其重新格式化为:

#define MyEnumDef \
    X(BOX), \
    X(RECT)

接下来的代码定义了枚举类型:

enum MyEnum
{
#define X(val) val
    MyEnumDef
#undef X
};

以下代码计算枚举元素的数量:

template <typename ... T> void null(T...) {}

template <typename ... T>
constexpr size_t countLength(T ... args)
{
    null(args...); //kill warnings
    return sizeof...(args);
}

constexpr size_t enumLength()
{
#define XValue(val) #val
    return countLength(MyEnumDef);
#undef XValue
}

...
std::array<int, enumLength()> some_arr; //enumLength() is compile-time
std::cout << enumLength() << std::endl; //result is: 2
...

通过从 #define MyEnumDef 中删除逗号(并将其放在 #define X(val) val 中),可以使此过程更加简单,这样您就可以仅使用 #define X(val) +1 constexpr std::size_t len = MyEnumDef; 来计算元素的数量。 - HolyBlackCat

5

Reflection TS:枚举(和其他类型)的静态反射

反射TS,尤其是最新版本的反射TS草案中的[reflect.ops.enum]/2提供了get_enumeratorsTransformationTrait操作:

[reflect.ops.enum]/2

template <Enum T> struct get_enumerators

All specializations of get_enumerators<T> shall meet the TransformationTrait requirements (20.10.1). The nested type named type designates a meta-object type satisfying ObjectSequence, containing elements which satisfy Enumerator and reflect the enumerators of the enumeration type reflected by T.

[reflect.ops.objseq]草案涵盖了ObjectSequence操作,特别是[reflect.ops.objseq]/1涵盖了get_size特性,用于提取满足ObjectSequence的元对象的元素数量:

[reflect.ops.objseq]/1

template <ObjectSequence T> struct get_size;

All specializations of get_size<T> shall meet the UnaryTypeTrait requirements (20.10.1) with a base characteristic of integral_constant<size_t, N>, where N is the number of elements in the object sequence.

因此,如果Reflection TS以其当前形式被接受和实现,枚举的元素数量可以在编译时计算,如下所示:
enum class Example { A, B, C, D, E };

using ExampleEnumerators = get_enumerators<Example>::type;

static_assert(get_size<ExampleEnumerators>::value == 5U, "");

我们可能会看到别名模板get_enumerators_vget_type_v,以进一步简化反射。

enum class Example { A, B, C, D, E };

using ExampleEnumerators = get_enumerators_t<Example>;

static_assert(get_size_v<ExampleEnumerators> == 5U, "");

Reflection TS的现状

根据Herb Sutter在2018年6月9日ISO C++委员会夏季会议的Trip report: Summer ISO C++ standards meeting (Rapperswil)中所述,Reflection TS已被宣布为功能完整。

Reflection TS是功能完整的:Reflection TS已被宣布为功能完整,并将在今年夏天进行主要评论投票。请注意,TS的当前模板元编程语法只是占位符;请求反馈的重点在于设计的核心“内涵”,委员会已经知道打算用普通的编译时代码而不是<>风格的元编程来替换表面语法。

最初计划将其纳入C++20,但目前尚不确定Reflection TS是否仍有机会进入C++20发布。


4

你可以尝试的一个技巧是在列表末尾添加一个枚举值,并将其用作大小。 在你的例子中

enum class Example { A, B, C, D, E, ExampleCount };

1
与普通的“枚举”行为相比,这种方法不起作用,因为“ExampleCount”是“Example”类型。要获取“Example”中元素的数量,必须将“ExampleCount”转换为整数类型。 - applesoup

2
如果您使用boost的预处理工具,可以使用BOOST_PP_SEQ_SIZE(...)获取计数。
例如,可以按以下方式定义CREATE_ENUM宏:
#include <boost/preprocessor.hpp>

#define ENUM_PRIMITIVE_TYPE std::int32_t

#define CREATE_ENUM(EnumType, enumValSeq)                                  \
enum class EnumType : ENUM_PRIMITIVE_TYPE                                  \
{                                                                          \
   BOOST_PP_SEQ_ENUM(enumValSeq)                                           \
};                                                                         \
static constexpr ENUM_PRIMITIVE_TYPE EnumType##Count =                     \
                 BOOST_PP_SEQ_SIZE(enumValSeq);                            \
// END MACRO   

然后,调用宏:
CREATE_ENUM(Example, (A)(B)(C)(D)(E));

会产生以下代码:
enum class Example : std::int32_t 
{
   A, B, C, D, E 
};
static constexpr std::int32_t ExampleCount = 5;

这只是涉及到 Boost 预处理器工具的表面。例如,您的宏还可以定义用于强类型枚举的字符串转换实用程序和 ostream 运算符。
更多关于 Boost 预处理器工具的信息,请参见 https://www.boost.org/doc/libs/1_70_0/libs/preprocessor/doc/AppendixA-AnIntroductiontoPreprocessorMetaprogramming.html
顺便说一下,我碰巧非常同意 @FantasticMrFox 的观点,即接受的答案使用的额外的Count枚举值将在使用switch语句时创建编译器警告头痛。我发现未处理的情况编译器警告对于更安全的代码维护非常有用,因此我不想削弱它。

@FantasticMrFox 感谢您指出被接受答案中存在的问题。我在这里提供了一种替代方法,更符合您建议的精神。 - arr_sea

2
不,您必须在代码中将其编写。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接