Item 10: 比起unscoped enum更偏愛scoped enum

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經遷移到這裏啦

一般情況下,在花括號中聲明一個name(包括變量名,函數名),這個name的可見性會被限制在花括號的作用域內。對於在C++98風格的enum中聲明的enum成員卻不是這樣。這些enum成員的name屬於的作用域是enum所在作用域,這意味着在這個作用域中,不能擁有相同的name:

enum Color { black, white, red };   //black,white,red
                                    //和Color在同一個作用域
auto white = false;                 //錯誤!white在這個
                                    //作用域已經聲明過了

所以事實上,這些enum成員name泄露到enum所在的作用域中去了,這導致官方對於這種enum給出了一個官方術語:unscoped。新的C++11中有一個與此相對應的版本:scoped enum,不會像這樣讓name泄露:

enum class Color { black, white, red }; //black,white red
                                        //在Color作用域中

auto white = false;                     //好的,沒其他white

Color c = white;                        //錯誤!在這個作用域中沒有
                                        //一個叫“white”的enum成員
Color c = Color::white;                 //對的

auto c = Color::white;                  //也是對的(而且和Item 5的建議一樣)

因爲scoped enum通過“enum class”聲明,它們有時候也被叫做enum類。

單是減少命名空間的污染就足夠作爲理由讓我們更偏愛scoped enum了,但是scoped enum還有第二個壓倒性的優點:它們的成員屬於強類型。unscoped enum的成員能隱式轉換到數值類型(然後,從數值類型,可以轉換到浮點類型)。因此像下面這樣,在語義上是扭曲的代碼是完全有效的:

enum Color { black, white, red};        //unscoped enum


std::vector<std::size_t>                //函數,返回x的素因數
    primeFactors(std::size_t x);

Color c = red;
...

if(c < 14.5) {                  //把Color和double數比較(!)

    auto factors =              //計算Color的素因數(!)
        primeFactors(c);        
    ...
}

然而,在”enum“後面添加一個簡單的”class“,就能把unscoped enum轉變爲scoped enum,並且情況會發生很大的改變。這裏沒有從enum的成員到任何其它類型的隱式轉換:

enum class Color { black, white, red }; //scoped

Color c = Color::red;                   
...

if (c < 14.5) {                 //錯誤,不能把Color和double
                                //數進行比較

    auto factors =              //錯誤,函數需要一個std::size_t
        primeFactors(c);        //不能傳一個Color進去      
    ...
}

如果你真的想要把Color轉換到不同的類型,你需要做的就是:使用cast把Color轉換成你需要的類型:

if(static_cast<double>(c) < 14.5) { //奇怪的代碼,但是有效

    auto factors =                  //可疑的,但是能通過編譯
        primeFactors(static_cast<std::size_t>(c));
    ...
}

比起unscoped enum,scoped enum看起來還有第三個優點,因爲scoped enum可以前置聲明。也就是,他們的name可以在聲明的時候不定義(不明確它們的成員):

enum Color;         //錯誤

enum class Color;   //對的

這是一個誤導。在C++11中,unscoped enum也能前置聲明,但是需要做一些額外的工作。這個工作源於一個事實,就是C++中的每個enum都有一個整形的基礎類型,這個類型由編譯器決定。對於一個unscoped enum,比如Color:

enum Color { black, white, red };

編譯器可能選擇char來作爲基礎類型,因爲這裏只有三個值需要表示。然而,一些enum值的範圍會大很多,比如:

enum Status { good = 0,
              failed = 1,
              incoplete = 100,
              corrupt = 200.
              indeterminate = 0xFFFFFFFF
            };

這裏enum值需要表示的範圍從0到0xFFFFFFFF。除了一些不尋常的機器(在這些機器中,一個char至少由32bits組成),編譯器就必須選擇一個大於char的整形類型來表示Status的值。

爲了內存的高效利用,只要一個基礎類型能成功表示enum中的成員的值的範圍,編譯器常常會選擇這個最小的基礎類型,。一些情況下,比起大小,編譯器會優先考慮速度,在這種情況下,它們不一定會選擇最小的基礎類型,但是它們肯定也會在考慮速度的優化後,考慮大小的優化。爲了實現這一點,C++98只支持定義(所有的enum成員必須列出來);enum的聲明是不被允許的。這樣,編譯器才能在每個enum使用前,給每個enum選擇一個基礎類型。

但是“不能前置聲明enum”是有缺點的。最需要注意的是,它可能會增加編譯依賴性。再次考慮Status enum:

enum Status { good = 0,
              failed = 1,
              incomplete = 100,
              corrupt = 200,
              indeterminate = 0xFFFFFFFF
            };

這個enum可能需要在某個系統中使用,因此它被包含在頭文件中,然後系統的每個部分都需要依賴它。如果一個新的status值被添加進來,

enum Status { good = 0,
              failed = 1,
              incomplete = 100,
              corrupt = 200,
              audited = 500,
              indeterminate = 0xFFFFFFFF
            };

這樣很有可能整個系統都需要重新編譯,甚至如果只是一個簡單的子系統(更甚一個簡單的函數)使用了這個enum。這是被人們所討厭的。這也是在C++11中,enum前置聲明消除的事(編譯依賴性)。舉個例子,這裏有一個scoped enum的聲明,它是完美有效的。並且一個函數用它作爲一個參數:

enum class Status;                  //前置聲明

void continueProcessing(Status s);  //使用聲明過的enum

如果Status的定義修改了,並且頭文件只包含這些聲明就不需要重新編譯。此外,如果Status修改了(比如,加了一個audited成員),但是continueProcessing的行爲沒受到影響(比如,因爲continueProcessing沒有使用audited),continueProcessing的實現也不需要重新編譯。

但是如果編譯器需要在一個enum使用前知道它的大小,C++11的enum怎麼就能使用前置聲明而C++98的enum卻不行?回答很簡單:scoped enum的基礎類型總是不變的,並且對於一個unscoped enum,你也能明確它的基礎類型。

通常情況下,一個scoped enum的基礎類型是int:

enum class Status;                  //基礎類型是int

如果默認的情況不適合你,你可以自己設置:

enum class Status: std::uint32_t;   //Status的基礎類型是
                                    //std::uint32_t

不管怎麼樣,編譯器都知道scoped enum的大小。

爲了明確一個unscoped enum的基礎類型,你需要做和scoped enum同樣的事,這樣就能做到前置聲明瞭:

enum Color: std::uint8_t;           //unscoped enum的前置聲明
                                    //基礎類型是std::uint8_t

基礎類型也可以在enum定義的時候明確:

enum class Status: std::uint32_t { good = 0,
                                   failed = 1,
                                   incomplete = 100,
                                   corrupt = 200,
                                   audited = 500,
                                   indeterminate = 0xFFFFFFFF
                                 };

基於scoped enum能避免命名空間的污染,不會進行無意義的隱式類型轉換的事實,這可能讓你感到奇怪:這裏起碼有一種情況,unscoped enum比起scoped enum會更有用一些。
比如我們在使用C++11的std::tuple的字段時。舉個例子,假設對於一個社交網站的用戶,我們想設計一個tuple持有name,email地址,reputation值:

using UserInfo =
    std::tuple<std::string,     //name
               std::string,     //email 
               std::size_t> ;   //reputation

只通過註釋標明每個字段,那麼在碰到分離的源文件時,註釋將沒有作用:

UserInfo uInfo;                 //tuple類型的對象
...

auto val = std::get<1>(uInfo);  //取字段1的值

作爲一個程序員,你有很多方式來記錄它。你真的能記住字段1代表的是用戶的email地址嗎?我想不是這樣的。使用一個unscoped enum把名字關聯到字段中去:

enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;                 
...

auto val = std::get<uiEmail>(uInfo);    //獲得email字段的值

要讓這起作用,必須要有從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>(UserInfoFields::uiEmail)>(uinfo);

繁瑣的情況可以通過寫一個函數(傳入一個enum成員,返回相應的std::size_t值)來減少,但是這裏有點微妙。std::get是一個template,並且你提供的值是template參數(注意使用尖括號,不是圓括號),所以轉換函數(enum成員到std::size_t)必須在編譯期產生結果。就像Item 15解釋的,這意味着它必須是constexpr函數。

事實上,它應該是constexpr函數template,因爲它應該能在任何enum下工作。並且如果我們讓它繼續泛化,我們應該把返回類型也泛化掉。比起返回一個std::size_t,我們需要返回enum的基礎類型。通過std::underlying_type能做到這一點。(看Item 9中的type traits信息)最後,我們聲明它爲noexcept(看Item 14),因爲我們知道它不會產生任何異常。最後會產生一個編譯期const函數template toUType,它需要一個任意類型的enum成員,並且返回它的值(類型是基礎類型):

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中,通過用強大的std::underlying_type_t(看Item 9)來替換typename std::underlying_type::type,toUType能被簡化:

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)也同樣有效:

template<typename E>                    // C++14
constexpr auto
  toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}   

不管函數怎麼寫,toUType允許我們這樣訪問tuple字段:

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

比起使用unscoped enum,這裏還是要寫很多東西,但是它也避免了命名空間的污染,和你沒有意識到的轉換。在很多情況下,比起它的陷阱,你可能覺得多寫幾個字是合理(但是追溯到很久以前我們的數字通信還在用2400波特的調制解調器時,情況會不一樣)。

            你要記住的事
  • C++98風格的enum被稱爲 unscoped enum
  • scoped enum的成員只在enum中可見,它們只有在使用cast時才能轉換到其它類型。
  • scoped 和unscopedenum都支持自定義基礎類型。scoped enum的默認基礎類型是int。unscoped enum沒有默認基礎類型。
  • scoped enum一直能前置聲明,unscoped enum只有在明確基礎類型的情況下才能前置聲明。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章