C++中将枚举量值映射到枚举量名的三种方法:使用Qt、手工映射与使用Better Enums

引子

最近遇到这样一种场景:为了方便调试Qt程序,需要对某些Qt控件的主要事件(鼠标事件、键盘事件和焦点事件等)进行日志记录。Qt每种事件类都是QEvent类的派生类,其具体类型可使用QEvent::type()方法获得,该方法返回一个QEvent::Type类型的枚举量。所以最基本的实现是这样的(假设要对Widget类的事件进行记录,用标准输出代替日志输出):

bool Widget::event(QEvent *e)
{
    cout << staticMetaObject.className() << " " << e->type() << endl;
    return QWidget::event(e);
}

这样就实现了最基本的记录事件的功能,但是这样做有一个很大的弊端:记录到日志中的都是枚举量值,也就是一个整数值。就像下面这样:

Widget 33
Widget 217
Widget 203
Widget 34
Widget 75
Widget 13
Widget 14
Widget 17
Widget 183

用这个整数值去查文档固然可以找到该值对应的事件类型,但还是麻烦了一些。那有没有什么方法可以直接把枚举量名记录下来呢?自然是有的。

方法1:使用Qt

(这种方法是针对于上面的场景的,需要基于Qt库,博主的Qt版本为5.10)
利用Qt的Meta-Object System可以很轻易的做到这点,Qt中的枚举量可以通过使用Q_ENUM宏进行注册,moc会将使用该宏注册的枚举量生成相应的QMetaEnum对象,我们可以利用该对象实现枚举量值与枚举量名之间的相互转换。从QEvent对象得到其类型枚举量名的实现如下(这里可以这么做是因为QEvent::Type已经使用了Q_ENUM进行注册):

static const char *EventTypeName(QEvent *e)
{
    //得到QEvent类的meta object
    const QMetaObject &metaObject = QEvent::staticMetaObject; 
    //根据枚举类型名得到对应的QMetaEnum对象(一个类中可能会注册过多个枚举类型,所以会有这一步)
    const QMetaEnum me = metaObject.enumerator(metaObject.indexOfEnumerator("Type")); 
    //将枚举量值转为枚举量名
    const char *name = me.valueToKey(e->type());
    return name != nullptr ? name : "Unknown";
}

利用这个函数,我们就可以很轻松的实现记录枚举量名的功能了。得到的输出如下:

Widget WindowTitleChange
Widget PlatformSurface
Widget WinIdChange
Widget WindowIconChange
Widget Polish
Widget Move
Widget Resize

利用Q_ENUM宏,你也可以对自定义的枚举量注册实现类似上面那样的功能,具体细节可查看Qt文档。
这种方式优点是简单,缺点是依赖Qt库,如果你的工作环境不基于Qt,就没办法这样做了。

方法2:手工映射

脱离上面的应用场景,也脱离Qt,该如何做到将枚举量值映射到枚举量名呢?不嫌代码丑的话可以手工映射:

#include 
#include 
#include 

using namespace std;

enum E_KEY
{
    KEY_A,
    KEY_B,
    KEY_C,
    KEY_D
};

static map<int, string> s_keyNameMap;

#define REGISTER(KEY) \
    s_keyNameMap.insert(pair<int, string>(KEY, #KEY));

void registerKey()
{
    REGISTER(KEY_A);
    REGISTER(KEY_B);
    REGISTER(KEY_C);
    REGISTER(KEY_D);
}

string getName(E_KEY key)
{
    return s_keyNameMap.at(key);
}

int main()
{
    registerKey();
    cout << getName(KEY_A) << "\n" << getName(KEY_B) << endl;
    return 0;
}

这种做法优点是不用依赖任何三方工具,缺点也很明显,比较繁琐,如果有几十个上百个枚举量时,一个一个注册就很费力了,枚举量如果添加删除值,registerKey函数也要同步修改。

方法3:使用Better Enums

还有一种方法就是利用一些轻量的第三方的工具库,比如标题中提到的Better Enums,只有一个头文件。它通过一些巨长无比的宏,实现枚举量值与枚举量值的映射,官方提供的样例如下,具体用法可访问其github主页:

#include 

BETTER_ENUM(Channel, int, Red = 1, Green, Blue)

Channel     c = Channel::_from_string("Red");
const char  *s = c._to_string();

size_t      n = Channel::_size();
for (Channel c : Channel::_values()) {
    run_some_function(c);
}

switch (c) {
    case Channel::Red:    // ...
    case Channel::Green:  // ...
    case Channel::Blue:   // ...
}

Channel     c = Channel::_from_integral(3);

constexpr Channel c =
    Channel::_from_string("Blue");

这种方式的优点是不需要挨个对枚举量手工映射,也不需要依赖Qt这样的重量级库。缺点是它是用一些很长的宏实现的,宏展开后的代码相当晦涩难懂,远不如Qt moc生成的代码好看,如果工具有bug的话,自己很难去debug。而且实际上,宏展开后枚举量并不是一个枚举量,而是一个类,这有没有可能带来一些副作用有待商榷,具体细节可以自行查看代码宏展开后的结果。

总结

当然还有其他方法了,stackoverflow的这个问题Is there a simple way to convert C++ enum to string下的回答中还有许多本文没有提到过的方法。但总的来说不外乎三种方法:
1. 手工映射
2. 使用(难懂的)宏,让标准的预处理器来帮你生成映射代码
3. 使用Qt moc这样的第三方工具帮你生成映射代码
这三种方法中我更倾向于方法3,如果没有Qt可用,自己造一个轮子应该也不会太难。

你可能感兴趣的:(Qt,C++)