以下内容为本人的学习笔记,如需要转载,请将本段内容(无删改)张贴于文章顶部:微信公众号「ENG八戒」mp.weixin.qq.com/s/2Kx5JTByZ…,更多无限制精彩内容欢迎跳转我的个人博客站点 ENG八戒
调试代码过程中,日志是非常重要的调试手段,为了在日志里体现上下文,经常需要把一些枚举量的当前值打印出来。
虽然枚举量可以直接打印为无符号整形值,但是对于枚举比较复杂的状态码来说,数值范围大,查看日志的时候就很考验记忆了,面对大量的上下文状态值,这有点尴尬。
于是,这个时候你就会想,能不能把枚举值对应的名字打印出来?不用再记忆枚举值和状态的映射,直接从枚举成员字符串就可以理解状态。
简单的方法
方法当然是有的,比较简单的就是直接针对已定义的枚举类型搞一个映射函数,该映射函数返回指定枚举值的字符串名:
enum OS_type { Linux, Apple, Windows };
inline const char* ToString(OS_type v) {
switch (v) {
case Linux: return "Linux";
case Apple: return "Apple";
case Windows: return "Windows";
default: return "[Unknown OS_type]";
}
}
这样子的函数很直观,可见每个枚举值都要指定映射结果。
但是,如果枚举类型内部定义有变更,比如新增或者删除枚举成员,甚至修改命名,那么对应的枚举映射函数也必须同步修改,所以简单的实现也带来了维护的难度。
有没有免去维护成本的方法?比如直接使用宏定义创建枚举类型,其它内容无须手动修改,全凭编译器管理:
宏(枚举类型名, 枚举值1/枚举值2/枚举值3/...)
boost 元编程库
boost 提供了 preprocessor 库用于操作宏展开,这为我们在编译期自动生成枚举定义和对应映射函数提供了便利。
BOOST_PP_SEQ_ENUM() 宏定义由 preprocessor 库提供,可以将枚举成员序列转换为枚举成员的定义范式。比如 BOOST_PP_SEQ_ENUM((Linux)(Apple)(Windows))
会被展开为 Linux, Apple, Windows
,这种特殊语法表示由 Boost.Preprocessor 定义。
所以,枚举定义的宏可以这样写:
#define DEFINE_ENUM(name, enumerators) \
enum name { \
BOOST_PP_SEQ_ENUM(enumerators) \
};
宏 DEFINE_ENUM 定义枚举类型,参数 name 代表枚举类型名,参数 enumerators 代表枚举成员序列。那么,假设需要定义一个枚举类型 OS_type,可以如此写:
DEFINE_ENUM(OS_type, (Linux)(Apple)(Windows))
Linux gcc 编译的时候加上 -save-temps
就可以在编译目录看到后缀为 .ii
的文件,里边的内容是经过预编译处理之后的代码,经过上面的宏调用会生成以下代码:
enum OS_type { Linux, Apple, Windows };
枚举定义是有了,可是返回枚举成员字符串名的映射函数呢?
回头看,上面简单方法实现中的函数 ToString(),它的主要部分是这样的构成:
case xxx_1: return "xxx_1";
case xxx_2: return "xxx_2";
...
default: return "[Unknown name]";
可见需要自动组装的代码无非就是生成 case 分支,不妨再来个宏专门组装 case 分支:
#define TOSTRING_CASE(elem) \
case elem : return BOOST_PP_STRINGIZE(elem);
调用宏 TOSTRING_CASE() 的时候 elem 代表枚举值。BOOST_PP_STRINGIZE() 宏定义由 preprocessor 库提供,实现将宏参数转换为其字符串表示,这里负责将枚举值转换为其字符串。
每调用一次宏 TOSTRING_CASE() 就生成一条 case 分支,所以还需要对每个枚举值逐个调用。
preprocessor 库提供了 BOOST_PP_SEQ_FOR_EACH() 实现遍历一个序列,并为序列中的每个元素调用一个宏。这里被调用的宏就可以是上面我们已经实现的 TOSTRING_CASE() 了。
BOOST_PP_SEQ_FOR_EACH() 基本形式如下:
BOOST_PP_SEQ_FOR_EACH(macro, data, seq)
其中:
- macro: 每当遇到序列中的一个元素时将要调用的宏。
- data: 传递给 macro 的额外数据,在每次调用 macro 时传递相同的参数即可,往往是多余的。
- seq: 一个序列,由 Boost.Preprocessor 定义的特殊语法表示,例如 (elem1)(elem2)(elem3)。
有了宏 BOOST_PP_SEQ_FOR_EACH() 的加持,所以 ToString() 函数也可以放在宏展开里快速实现。为了简化枚举的声明,同时将 ToString() 的实现集成到宏 DEFINE_ENUM() 定义里:
#define DEFINE_ENUM(name, enumerators) \
enum name { \
BOOST_PP_SEQ_ENUM(enumerators) \
}; \
\
inline const char* ToString(name v) { \
switch (v) { \
BOOST_PP_SEQ_FOR_EACH( \
TOSTRING_CASE, \
name, \
enumerators) \
default: return "[Unknown " BOOST_PP_STRINGIZE(name) "]"; \
} \
}
到此,以后每次需要定义一个枚举类型,就直接调用宏 DEFINE_ENUM() 就好:
DEFINE_ENUM(OS_type, (Ubuntu)(MacOS)(Windows))
int main()
{
OS_type t = Windows;
std::cout << ToString(t) << " " << ToString(MacOS) << std::endl;
}
上面定义了一个名为 OS_type 的枚举类型,枚举值有三个 Ubuntu, MacOS, Windows
。
看起来这样的宏定义不仅自带实现映射函数,还让枚举的声明定义依然保持简便,就算修改枚举类型的定义也无需手动维护映射函数的更新。
如果你看到了示例代码突然多了个疑问,请揣兜里备好,文末有讨论。
看看宏展开之后的实际代码,编译之后打开后缀为 .ii
的文件:
enum OS_type { Ubuntu, MacOS, Windows };
inline const char* ToString(OS_type v) {
switch (v) {
case Ubuntu : return "Ubuntu";
case MacOS : return "MacOS";
case Windows : return "Windows";
default: return "[Unknown " "OS_type" "]";
}
}
看到展开的代码基本和手动实现的 ToString() 一模一样。
上面是使用了 boost 提供的 preprocessor 库的实现思路,要注意 boost 并不是 C++ 标准库内容,如果你的环境不支持 boost 呢?
有没有更多的方法实现呢?留意下期文章继续
欢迎开放性讨论,集思广益,笔者很希望可以和你进一步探讨这方面的问题。
英雄所见有所不同,请来评论区一吐为快!
另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。上拉到文章顶部有我的联系方式!