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 <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可用,自己造一個輪子應該也不會太難。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章