比起unscoped enums更偏愛scoped nums
作爲一個通用的規則,在大括號內聲明一個名字就限制了名字的作用域在括號之內。這個規則不適用於C++98的枚舉。這種枚舉的枚舉值的作用域與枚舉enum的作用域相同:
enum Color { black, white, red }; // black while red的作用域與Color相同
auto white = false; // 錯誤,當前作用域已經聲明瞭white
事實上,這些枚舉的值會從它們的枚舉(enum)中泄露出來,引出了這種enum的正式風格:unscoped 。在C++11中,有它的相對物,scoped enum,不會造成這種泄露值的問題:
enum class Color { black, white, red }; // black white red作用域在Color內
auto white = false; // 沒有錯誤,沒有其他的“white”在當前作用域
Color c = white; // 錯誤,當前作用域沒有“white”這個枚舉的值
// 只有bool類型的“white”
Color c = Color::white; // 正確
auto c = Color::white; // 也正確(根據條款5的建議)
因爲scoped enum通過“enum class”來聲明,所以它們指代的是枚舉類。
scoped enum減少命名空間污染已經足夠說服你用scoped enum代替unscoped enum了,不過scoped enum還有一個讓你不得不使用它的優勢:它枚舉的名字具有更健壯的類型。unscoped enum枚舉的值隱式地轉換爲整型數類型(或者浮點數類型)。一些語義上的歪曲也是完全可行的,如下:
enum Color { black, white, red }; // unscoped enum
std::vector<std::size_t> primeFactors(std::size_t); // 返回質因數
Color c = red;
...
if (c < 14.5) { // 比較Color和浮點數
auto factors = primeFactors(c); //計算Color的質因數
...
}
但是,在enum之後加個class,unscoped enum轉換成scoped enum,這樣就是另一個不同的故事啦。scoped enum枚舉的值無法被隱式轉換爲其他類型:
enum class Color { black, white, red }; // scoped enum
Color c = Color::red;
...
if (c < 14.5) { // 錯誤, 無法與Color和double比較
auto factors = primeFactors(c); // 錯誤
... // 無法把Color傳遞給參數爲size_t的函數
}
如果你確實想要把Color轉換爲不同的類型,你應該說明你想要的類型——使用cast:
if (static_cast<double>(c) < 14.5) {
auto factors = primeFactors(static_cast<std::size_t>(c));
...
}
好像scoped enum比起unscoped enum還有第三個優點,scoped enum可以前向聲明(forward-declaration),它可以不帶值地聲明枚舉的名字:
enum Color; // 錯誤
enum class Color; // 正確
不過這是誤導人的。在C++11中,unscoped enum也可以前向聲明瞭,不過需要做些額外的工作。這些額外的工作來源於每個enum值都有一個整數的基礎類型(underlying type),而編譯器要決定它們,例如:
enum Color { black, white, red };
編譯器可能選擇char作爲潛在類型,因爲只需表示3個值。但是,一些枚舉值的範圍會很大,例如:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
這裏的值的範圍從0~0xFFFFFFFF。就算是特殊的機器(char佔32位),編譯器也會選擇比char類型更大的類型來表示Status的值。
爲了高效實用內存,編譯器一般選擇足夠表示枚舉值範圍的最小基礎類型。在一些例子中,編譯器會以速度爲優化目的,而不是空間,此時編譯器可能不會選擇允許的最小基礎類型,但它們肯定也想要優化空間使用。爲了這個目的,C++98只允許定義enum(列出枚舉值),而不允許聲明enum。這使得編譯器在使用 枚舉值之前選擇基礎類型成爲可能。
但是enum前向聲明使得編譯器無能爲力。最可能實現的是增加編譯依賴,再次思考Status:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
這個枚舉好像會在一個系統中多處被使用,所以把它包含在頭文件中。如果我們新引入了個枚舉值:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500, // 新引入
indeterminate = 0xFFFFFFFF
};`
這樣的話可能整個系統都要重新編譯了,就算只有一個子系統用到這個新引入的枚舉值,就算只有一個函數用到。人們是非常厭惡這樣的事情的。C++11的枚舉前向聲明能力可以消除這樣的問題。例如,scoped enum這樣的聲明和一個接收它作爲參數的函數:
enum class Status; // 前向聲明
void continueProcessing(Status s); // 使用前向聲明的枚舉
如果頭文件包含這樣的聲明,那麼修改Status的定義時,就不需要重新編譯了。而且,如果Status(例如引入audited)被修改,continueProcessing函數的行爲不會受到影響(因爲函數沒有使用audited),那麼函數的實現也無需重新編譯。
但是編譯器使用枚舉值之前需要知道枚舉的基礎類型。C++11的enum是如何僥倖使用前向聲明的呢,C++98卻不行。答案很簡單:scoped enum的基礎類型總是已知的,而對於unscoped enum需要指定它的類型。
scoped enum默認的基礎類型是int:
enum class Status;
// 基礎類型是int
如果默認的類型不適合你,你可以重寫它:
enum class Status: std::uint32_t;
// Status的基礎類型是std::uint32
不管哪一種方式,編譯器都會知道scoped enum枚舉值的大小。
和上面一樣,說明unscoped enum的基礎類型,還可以前向聲明:
enum Color: std::uint8_t;
// unscoped enum前向聲明
基礎類型指定也可以在枚舉定義時使用:
enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
鑑於scoped enum可以避免命名空間污染,和不容易被無意義到隱式類型轉換影響,如果告訴你不止一種情況下unscoped enum更有用,你可能會驚呆。這涉及到C++11的std::tuple。例如,我們有一個tuple持有名字、郵箱、在某社交網站上的名譽值:
using UserInfo = std::tuple<std::string, std::string, std::size_t>;
// 使用了using, 見條款9
// 參數分別是名字,郵箱,聲名值
儘管註釋中表明tuple每一區域表示的含義,但是你在單獨的源碼文件中遇到這樣的代碼,你會覺得毫無幫助:
UserInfo uInfo; // tuple對象
...
auto val = std::get<1>(uInfo); // 獲取uInfo的第1個值
作爲一個程序員,你有許多方法來追蹤它。難道你真的要去記UserInfo的第1個值是郵箱嗎?我覺得不用阿。使用unscoped enum來關聯名字和數字就可以避免這個問題啦:
enum UserInfoFields {uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo); // 取得郵箱的值
這樣可行的原因是UserInfoFields的值隱式轉換爲std::size_t,也就是std::get需求的類型。
而scoped enum實現就囉嗦了點:
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val =
std::get<static_cast<std::size_t>(UserInfoFileds::uiEmail)>
(uInfo);
這種冗長的代碼可以寫一個函數來減少,這個函數接受枚舉值爲參數,返回其相對的std::size_t值,但這有點複雜。std::get是一個模板,然後你傳遞給它的值是模板參數(注意它使用的是方括號,不是圓括號),所以這個轉換枚舉值到std::size_t的函數必須在編譯期間得到結果。根據條款15,這意味着它是個constexpr函數。
事實上,它應該是個constexpr模板函數,因爲它應該可以爲多種枚舉工作。然後如果我們想將它一般化,我們也應該把返回類型一般化。比起返回std::size_t,我們將返回枚舉的基礎類型,可以藉助std::underlying_type得到。最後我們還要把它聲明爲noexcept(看條款14),因爲我們知道它永遠都不會拋出異常。最後就成了這個樣子:
template <typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return
static_cast<typename
std::underlying_type<E>::type>(enumerator);
}
在C++14中,“typename std::underlying_type::type”可以用時髦的“std::underlying_type_t”來代替(見條款9,本人省略了這部分):
template <typename E>
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
更時髦地返回auto在C++14中也是可行的:
template <typename E>
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
不管toUType怎麼實現,toUType允許我們這樣獲取tuple中值:
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
這還是比unscoped enum寫得多,不過這樣還可以避免命名空間污染和枚舉值的不經意的類型轉換。在許多情況下,你可能覺得寫多一些字母來避免一下枚舉上的陷阱是值得理解的。
總結
需要記住的4點:
- C++98風格的enum現在被稱爲unscoped enum。
- scoped enum的枚舉值只在enum內可見,它們只能通過cast來轉換成其他類型。
- scoped enum和unscoped enum都支持指定基礎類型(underlying type),scoped enum的默認基礎類型是int,unscoped enum沒有默認基礎類型。
- scoped enum總是可以前向聲明,unscoped enum只有在它聲明時指定了基礎類型纔可以前向聲明。