Item 10:偏好 scoped enums 而非 unscoped enums (Prefer Scoped Enums to Unscoped Enums)
Overview Table
| 面向 | unscoped enum(C++98 風格) | scoped enum(enum class,C++11) |
|---|---|---|
| 名稱作用域 | 列舉子洩漏到 enum 所在的外層 scope | 列舉子只在 enum 內可見,需 Color::white 存取 |
| 隱含轉換 | 隱含轉成整數型別(再轉浮點) | 無任何隱含轉換,必須 static_cast |
| 預設底層型別 | 沒有(由編譯器針對每個 enum 挑選) | 預設為 int |
| 指定底層型別 | 可以(enum Color: std::uint8_t) |
可以(enum class Status: std::uint32_t) |
| 前向宣告 | 只有在指定底層型別時才可以 | 永遠可以 |
| 典型優勢情境 | std::tuple 欄位索引(隱含轉換反而方便) |
幾乎所有其他情境 |
名稱洩漏:unscoped 汙染外層作用域
C++98 enum 的列舉子屬於包含 enum 的作用域,因此同名衝突直接編譯失敗;scoped enum 把名稱關進大括號裡。
enum Color { black, white, red }; // unscoped:black、white、red
// 與 Color 同屬外層 scope
auto white = false; // 錯誤!white 已在此 scope 宣告
enum class Color { black, white, red }; // scoped:名稱限縮在 Color 內
auto white = false; // OK,此 scope 沒有別的 white
Color c = white; // 錯誤!此 scope 沒有叫 white 的
// 列舉子(white 是上面的 bool)
Color c2 = Color::white; // OK,加上作用域限定
auto c3 = Color::white; // 也 OK(符合 Item 5 用 auto 的建議)
因為以 enum class 宣告,scoped enum 又常被稱為 enum classes。「減少 namespace 汙染」本身就足以構成偏好它的理由。
強型別:scoped enum 不做隱含轉換
unscoped enum 的列舉子會隱含轉換成整數型別(進而轉成浮點),導致語意荒謬的程式碼照樣合法:
enum Color { black, white, red }; // unscoped
std::vector<std::size_t> primeFactorssize_t x;
Color c = red;
if (c < 14.5) { // 拿 Color 和 double 比較(!)竟然合法
auto factors = primeFactors(c); // 對 Color 求質因數(!)也合法
}
改成 scoped enum 後,上述兩行全部變成編譯錯誤;若真的需要轉換,必須明確寫出 cast:
enum class Color { black, white, red }; // scoped
Color c = Color::red;
if (static_cast<double>(c) < 14.5) { // 怪,但合法
auto factors =
primeFactorssize_t>(c); // 可疑,但能編譯
}
底層型別與前向宣告
每個 enum 都有整數底層型別(underlying type)。C++98 只允許 enum「定義」(列出全部列舉子),讓編譯器得以挑選最省空間的底層型別,代價是無法前向宣告,造成編譯依賴:
C++98 unscoped enum(不能前向宣告)
Status 定義(header)── 被整個系統 #include
│ 新增一個列舉子(如 audited = 500)
▼
★ 整個系統全部重新編譯
C++11 scoped enum(可前向宣告)
header: enum class Status; // 宣告即可,不含列舉子
void continueProcessing(Status s);
│ Status 定義變動
▼
★ 只有真正用到新列舉子的檔案需要重編
前向宣告可行的關鍵:scoped enum 的底層型別永遠已知(預設 int);unscoped enum 則必須手動指定底層型別後才能前向宣告。
enum Color; // 錯誤!unscoped 不能直接前向宣告
enum class Status; // OK,底層型別預設為 int
enum class Status: std::uint32_t; // 覆寫底層型別(<cstdint>)
enum Color: std::uint8_t; // OK!指定底層型別後 unscoped 也能前向宣告
enum class Status: std::uint32_t { // 底層型別也可寫在定義上
good = 0, failed = 1, incomplete = 100,
corrupt = 200, audited = 500, indeterminate = 0xFFFFFFFF
};
正確說法是:scoped enum 永遠可前向宣告;unscoped enum 只有在宣告中指定底層型別時才可前向宣告。這是本 Item「Things to Remember」的原句,面試常考反例。
例外情境:std::tuple 欄位索引與 toUType
取用 std::tuple 欄位時,unscoped enum 對 std::size_t 的隱含轉換反而讓程式碼更簡潔——這是原書承認 unscoped enum 仍有用武之地的唯一情境。
using UserInfo = std::tuple<std::string, // name
std::string, // email
std::size_t>; // reputation
enum UserInfoFields { uiName, uiEmail, uiReputation }; // unscoped
auto val = std::get<uiEmail>(uInfo); // 隱含轉成 std::size_t,簡潔
enum class UserInfoFields2 { uiName, uiEmail, uiReputation }; // scoped
auto val2 = std::get<static_cast<std::size_t>( // 冗長許多
UserInfoFields2::uiEmail)>(uInfo);
想兼得 scoped enum 的安全與簡潔,可寫一個 constexpr 函式模板 toUType:因為 std::get 是模板、需要編譯期的模板引數,所以函式必須是 constexpr(見 Item 15);回傳型別用 std::underlying_type type trait 取得(見 Item 9);並宣告 noexcept(見 Item 14):
template<typename E> // C++14 版本
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept // 絕不拋例外
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
// C++14 也可用 auto 回傳型別(見 Item 3);
// C++11 需寫 typename std::underlying_type<E>::type
auto val = std::get<toUTypeuiEmail)>(uInfo; // 安全又可讀
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| C++98 風格 enum 的正式名稱 | unscoped enum(列舉子洩漏到外層 scope) |
enum class 列舉子的可見範圍 |
只在 enum 內;外部需 EnumName::enumerator |
if (color < 14.5) 能編譯? |
unscoped:能(隱含轉換);scoped:編譯錯誤 |
| scoped enum 轉成整數的唯一方法 | cast(如 static_cast<int>(c)) |
| scoped enum 預設底層型別 | int;unscoped enum 沒有預設(編譯器自選) |
| 哪種 enum 永遠可前向宣告 | scoped enum;unscoped 需指定底層型別才行 |
| enum 前向宣告的最大好處 | 降低編譯依賴,改定義不必全系統重編 |
std::get<> 搭配 enum 索引 |
unscoped 靠隱含轉換最簡潔;scoped 用 constexpr toUType |
toUType 需要哪些關鍵字/工具 |
constexpr + std::underlying_type_t + noexcept |
Related Notes
- 04-Moving-To-Modern-Cpp/03-Alias-Declarations:
toUType使用的std::underlying_type_t是 Item 9 介紹的 alias template 版 type trait - 04-Moving-To-Modern-Cpp/09-Constexpr:
std::get的模板引數需要編譯期常數,toUType因此必須是 constexpr 函式 - 04-Moving-To-Modern-Cpp/08-Noexcept:
toUType宣告noexcept的理由——確知不會拋出例外 - 03-Auto/01-Prefer-Auto-To-Explicit-Types:
auto c = Color::white;呼應 Item 5 偏好 auto 的建議 - 02-Deducing-Types/03-Decltype:C++14
toUType可改用 auto 回傳型別(Item 3 的推導規則)