以枚举为例,探究一下如何实现C++反射机制「下」

97 阅读4分钟

以下内容为本人的学习笔记,如需要转载,请将本段内容(无删改)张贴于文章顶部:微信公众号「ENG八戒」mp.weixin.qq.com/s/J95AumvaS…,更多无限制精彩内容欢迎查阅我的个人博客站点 ENG八戒

_9940fa9f-e252-4132-acef-a6ee85ccd3fd.jpeg

本文上接《以枚举为例,探究一下如何实现C++反射机制「上」》

标准实现

精彩内容继续,为了避开第三方库的特殊能力,我们还需要完全基于标准库的能力来实现类似上面的宏定义 DEFINE_ENUM()。

依赖库是一方面,更重要的是声明枚举值时的特殊语法 (elem1)(elem2)(elem3) 真的比较怪。语法是依赖库的规定,避免不了,想要更灵活的表达方式还是需要自己实现。

第一步,还是定义枚举类型。

列举声明枚举值使用逗号分隔才是常规写法,而且枚举成员数量需要依据业务逻辑需求指定,因而可采用不定长参数的宏定义:

#define DECLARE_ENUM(ename, ...)        \
    enum ename { __VA_ARGS__, COUNT };

ename 代表枚举类型名,后边的不定长参数用于接收数量不固定的枚举成员。所以调用宏定义 DECLARE_ENUM 时需要注意第一个输入参数固定为枚举类型名,其后的所有剩余输入参数都为枚举成员。

在宏定义中 __VA_ARGS__ 代指不定长的参数列表,在枚举声明成员的末尾,额外添加了一个成员 COUNT 用于记录枚举成员的数量。

这样在调用宏 DECLARE_ENUM 定义枚举类型 OsType 时,如此:

DECLARE_ENUM(OsType, Windows, Ubuntu, MacOS)

Windows, Ubuntu, MacOS 列举枚举有三个成员,这样的写法自然很多。

要实现类似 ToString() 的函数,必须能对宏 DECLARE_ENUM 接收到的成员序列进行指定顺序提取,不定长参数就是成员序列。

在预编译阶段,可以比较方便地将不定长参数整体转换为字符串,每个参数之间又有固定分隔符逗号(,),所以可以先将不定长参数作为字符串序列存储在数组或容器中。

由于上面在声明枚举成员的末尾添加了一个额外的成员 COUNT 用于记录枚举成员的数量,所以可以很方便地确定数组长度,为效率起见就用数组吧:

static std::string _Strings[COUNT];

然后在调用 ToString() 函数执行转换时,从数组或容器读取即可,这里选择从数组:

const char* ToString(ename e) {
    if (_Strings[0].empty()) {
        SplitEnumArgs(#__VA_ARGS__, _Strings, COUNT);
    }
    return _Strings[e].c_str();
}

#__VA_ARGS__ 将不定长参数整体转换为字符串,函数 SplitEnumArgs() 对不定长参数的整体字符串执行分隔提取并存储在数组 _Strings 中,限制最多提取字符串数量为 COUNT。

提取过程是很耗时间的,提取下来的子字符串有保存在缓冲中,没必要每次调用 ToString() 函数执行转换时都提取一次,所以仅在未提取(缓冲为空)时触发提取(调用 SplitEnumArgs)。

void SplitEnumArgs(const char* szArgs,
                    std::string Array[],
                    int nMax) {
    std::istringstream iss(szArgs);
    int nIdx = 0;
    while (nIdx < nMax) {
        std::string strSub;
        std::getline(iss, strSub, ',');
        Array[nIdx] = TrimEnumString(strSub);
        ++ nIdx;
    }
}

在函数 SplitEnumArgs() 内部,先将输入字符串用流 std::istringstream 管理,然后使用 std::getline 依据分隔符逗号(,)逐段从字符串提取子字符串,提取后再使用函数 TrimEnumString() 将子字符串的首尾空白字符都去掉。

思路就是这样。

但实际使用中应该会存在变量和函数同名的情况,这会产生冲突,为了安全起见,应该引入命名空间,如:

#define DECLARE_ENUM(ename, ...)                                \
    namespace ename {                                           \
        enum ename { __VA_ARGS__, COUNT };                      \
        static std::string _Strings[COUNT];                     \
        const char* ToString(ename e) {                         \
            if (_Strings[0].empty()) {                          \
                SplitEnumArgs(#__VA_ARGS__, _Strings, COUNT);   \
            }                                                   \
            return _Strings[e].c_str();                         \
        }                                                       \
    }

这样定义的枚举类型和一众相关变量、函数等都被放置于同名的命名空间内。

思考

为了将枚举量转换成字符串,上面介绍了三种方式。

第一种最简单直接,但是缺点也比较明显,维护麻烦。

第二种大为改进,但是语法怪异,不太喜欢。

第三种貌似已经完美,但是如果你大量应用它在实际代码中,会发现它无法适用于下面这种情况:

DECLARE_ENUM(OsType, Windows, Ubuntu = 50 , MacOS)

是的,如果枚举成员列表包含了赋值表达式,那么无论是第二种方法还是第三种方法中已实现的示例代码都不能适用。

为了适配枚举成员列表包含了赋值表达式这种情况,第三种方法能不能继续改进?如何实现?

笔者已经实现这个适配,代码比较长,请在原文链接中获取。


欢迎开放性讨论,集思广益,笔者很希望可以和你进一步探讨这方面的问题。

英雄所见有所不同,请来评论区一吐为快!

另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。上拉到文章顶部有我的联系方式!