Effective Modern C++ 條款10 比起unscoped enums更偏愛scoped enums

比起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之後加個classunscoped 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 enumunscoped enum都支持指定基礎類型(underlying type),scoped enum的默認基礎類型是intunscoped enum沒有默認基礎類型。
  • scoped enum總是可以前向聲明,unscoped enum只有在它聲明時指定了基礎類型纔可以前向聲明。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章