C++获取枚举元素数量

最近有个特殊需求,需要获取一个枚举类型的元素数量,于是学到了以下骚操作。

解释

操作源自网站stack overflow的一个提问《是否可以确定 c++ 枚举类的元素数量?》。

手艺人

基础

首先是最简单的:

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

由于枚举值默认从0递增,通过static_cast<int>(Example::Count)就可以获取数量。

进阶

上述方法不适用于有自定义值的枚举,例如:

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

需要手动计数无疑很不便且枯燥。

傻瓜

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
enum class Example { A, B, C, D, E };

constexpr int ExampleCount = [] {
  Example e{};
  int count = 0;
  switch (e) {
    case Example::A:
      count++;
    case Example::B:
      count++;
    case Example::C:
      count++;
    case Example::D:
      count++;
    case Example::E:
      count++;
  }

  return count;
}();

仅表示对此实现的看法:更多的代码意味着更多的错误。

评价

当你对这个枚举类使用switch时,编译器会警告你缺少一个case,不优雅。

另外上述方式适用面窄且让人成为了程序的最大风险,老子曰过:“凡可自动化,皆使自动化”。

宏魔法

__LINE__宏定义

众所周知__LINE__表示当前行号,那么编写如下代码:

1
2
3
4
5
6
7
8
9
// 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

通过行号相减,并关闭clang-format确保代码格式化不会破坏格式,即可得出元素数量。

GCC非标宏__COUNTER__

__COUNTER__ 是 GNU 编译器的非标准编译器扩展。 可以认为它是一个计数器,代表一个整数,它的值一般被初始化为0,在每次编译器编译到它时,会自动 +1

那么可以实现如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;

Boost

果使用 boost 的预处理器实用程序,则可以使用 BOOST_PP_SEQ_SIZE(...) 获取计数。

例如,可以按如下方式定义 CREATE_ENUM 宏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#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   

然后,调用宏:

1
CREATE_ENUM(Example, (A)(B)(C)(D)(E));

宏展开将生成以下代码:

1
2
3
4
5
enum class Example : std::int32_t 
{
   A, B, C, D, E 
};
static constexpr std::int32_t ExampleCount = 5;

这只是 boost 预处理器工具的冰山一角。例如,宏还可以为强类型枚举定义 to/from 字符串转换实用程序和 ostream 运算符。

阅读有关 boost 预处理器工具的更多信息

利用可变参数__VA_ARGS__

1
2
3
4
5
6
7
8
#define Enum(Name, ...)                                                        \
    struct Name {                                                              \
        enum : int {                                                           \
            __VA_ARGS__                                                        \
        };                                                                     \
        private: struct en_size { int __VA_ARGS__; };                          \
        public: static constexpr  size_t count = sizeof(en_size)/sizeof(int);  \
    }

用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct S {

    Enum(TestEnum, a=11, b=22, c=33);

    void Print() {
        std::cout << TestEnum::a << '\n';
        std::cout << TestEnum::b << '\n';
        std::cout << TestEnum::count << '\n';
    }

};


int main()
{        

    S d;
    d.Print();

    return 0
}

输出:

1
2
3
11
22
3

全场最佳——依旧是可变参数__VA_ARGS__

这可以通过 std::initializer_list 的技巧来解决:

1
2
3
4
5
6
7
8
9
#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__}); \
    }();                                                          \
};

用法:

1
2
3
4
5
6
7
8
#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;
}

反射未来可期

C++ Reflection Technical Specification(反射技术规范,以下简称反射TS),特别是最新版本的反射 TS 草案的 [reflect.ops.enum]/2,提供了 get_enumeratorsTransformationTrait 操作:

[reflect.ops.enum]/2

1
template <Enum T> struct get_enumerators

get_enumerators<T> 的所有特化都应满足 TransformationTrait 要求 (20.10.1)。名为 type 的嵌套类型指定满足 ObjectSequence 的元对象类型,其中包含满足 Enumerator 并反射 T 所反映的枚举类型的枚举数的元素。

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

[reflect.ops.objseq]/1

1
template <ObjectSequence T> struct get_size;

get_size<T> 的所有特化都应满足 UnaryTypeTrait 要求 (20.10.1),其基本特征为 integral_constant<size_t,N>,其中 N 是对象序列中的元素数。

因此,在 Reflection TS 中,要以当前形式接受和实现,枚举的元素数可以在编译时计算,如下所示:

1
2
3
4
5
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 以进一步简化反射:

1
2
3
4
5
enum class Example { A, B, C, D, E };

using ExampleEnumerators = get_enumerators_t<Example>;

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

正如 Herb Sutter 的旅行报告所述:夏季 ISO C++ 标准会议 (Rapperswil) 从 2018 年 6 月 9 日的 ISO C++ 委员会夏季会议开始,Reflection TS 已被宣布为功能完成

Reflection TS 功能齐全:Reflection TS 已宣布功能完整,并将在夏季进行主要意见投票。再次注意,TS 当前基于模板元编程的语法只是一个占位符;所请求的反馈是关于设计的核心 “guts” 的,委员会已经知道它打算用一种更简单的编程模型来取代表面语法,该模型使用普通的编译时代码,而不是 <> 风格的元编程。

最初计划用于 C++20,但目前尚不清楚 Reflection TS 是否仍有机会进入 C++20 版本。

引用