引子
最近遇到這樣一種場景:爲了方便調試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 <iostream>
#include <map>
#include <string>
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 <enum.h>
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可用,自己造一個輪子應該也不會太難。