條款10:優先使用作用域限制的`enmus`而不是無作用域的`enum`
=========================
一般而言,在花括號里面聲明的變量名會限制在括號外的可見性。但是這對于`C++98`風格的`enums`中的枚舉元素并不成立。枚舉元素和包含它的枚舉類型同屬一個作用域空間,這意味著在這個作用域中不能再有同樣名字的定義:
```cpp
enum Color { black, white, red}; // black, white, red 和
// Color 同屬一個定義域
auto white = false; // 錯誤!因為 white
// 在這個定義域已經被聲明過
```
事實就是枚舉元素泄露到包含它的枚舉類型所在的作用域中,對于這種類型的`enum`官方稱作無作用域的(`unscoped`)。在`C++11`中對應的使用作用域的enums(`scoped enums`)不會造成這種泄露:
```cpp
enum class Color { black, white, red}; // black, white, red
// 作用域為 Color
auto white = false; // fine, 在這個作用域內
// 沒有其他的 "white"
Color c = white; // 錯誤!在這個定義域中
// 沒有叫"white"的枚舉元素
Color c = Color::white; // fine
auto c = Color::white; // 同樣沒有問題(和條款5
// 的建議項吻合)
```
因為限制作用域的`enum`是通過"enum class"來聲明的,它們有時被稱作枚舉類(`enum class`)。
限制作用域的`enum`可以減少命名空間的污染,這足以是我們更偏愛它們而不是不帶限制作用域的表親們。除此之外,限制作用域的`enums`還有一個令人不可抗拒的優勢:它們的枚舉元素可以是更豐富的類型。無作用域的`enum`會將枚舉元素隱式的轉換為整數類型(從整數出發,還可以轉換為浮點類型)。因此像下面這種語義上荒誕的情況是完全合法的:
```cpp
enum Color { black, white, red }; // 無限制作用域的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"`,就可以將一個無作用域的`enum`轉換為一個有作用域的`enum`,變成一個有作用域的`enum`之后,事情就變得不一樣了。在有作用域的`enum`中不存在從枚舉元素到其他類型的隱式轉換:
```cpp
enum class Color { black, white, red }; // 有作用域的enum
Color c = Color::red; // 和前面一樣,但是
... // 加上一個作用域限定符
if (c < 14.5){ // 出錯!不能將Color類型
// 和double類型比較
auto factors = // 出錯!不能將Color類型傳遞給
primeFactors(c); // 參數類型為std::size_t的函數
...
}
```
如果你就是想將`Color`類型轉換為一個其他類型,使用類型強制轉換(`cast`)可以滿足你這種變態的需求:
```cpp
if(static_cast<double>(c) < 14.5) { // 怪異但是有效的代碼
auto factors = // 感覺不可靠
primeFactors(static_cast<std::size_t(c)); // 但是可以編譯
...
}
```
相較于無定義域的`enum`,有定義域的`enum`也許還有第三個優勢,因為有定義域的`enum`可以被提前聲明的,即可以不指定枚舉元素而進行聲明:
```cpp
enum Color; // 出錯!
enum class Color; // 沒有問題
```
這是一個誤導。在`C++11`中,沒有定義域的`enum`也有可能被提前聲明,但是需要一點額外的工作。這個工作時基于這樣的事實:`C++`中的枚舉類型都有一個被編譯器決定的潛在的類型。對于一個無定義域的枚舉類型像`Color`,
```cpp
enum Color {black, white, red };
```
編譯器有可能選擇`char`作為潛在的類型,因為僅僅有三個值需要表達。然而一些枚舉類型有很大的取值的跨度,如下:
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
```
這里需要表達的值范圍從`0`到`0xFFFFFFFF`。除非是在一個不尋常的機器上(在這臺機器上,`char`類型至少有`32`個`bit`),編譯器一定會選擇一個取值范圍比`char`大的整數類型來表示`Status`的類型。
為了更高效的利用內存,編譯器通常想為枚舉類型選擇可以充分表示枚舉元素的取值范圍但又占用內存最小的潛在類型。在某些情況下,為了代碼速度的優化,可以回犧牲內存大小,在那種情況下,編譯器可能不會選擇占用內存最小的可允許的潛在類型,但是編譯器依然希望能過優化內存存儲的大小。為了使這種功能可以實現,`C++98`僅僅支持枚舉類型的定義(所有枚舉元素被列出來),而枚舉類型的聲明是不被允許的。這樣可以保證在枚舉類型被用到之前,編譯器已經給每個枚舉類型選擇了潛在類型。
不能事先聲明枚舉類型有幾個不足。最引人注意的就是會增加編譯依賴性。再次看看`Status`這個枚舉類型:
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
```
這個枚舉體可能會在整個系統中都會被使用到,因此被包含在系統每部分都依賴的一個頭文件當中。如果一個新的狀態需要被引入:
```cpp
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
```
就算一個子系統——甚至只有一個函數!——用到這個新的枚舉元素,有可能導致整個系統的代碼需要被重新編譯。這種事情是人們憎恨的。在`C++11`中,這種情況被消除了。例如,這里有一個完美的有效的有作用域的`enum`的聲明,還有一個函數將它作為參數:
```cpp
enum class Status; // 前置聲明
void continueProcessing(Status s); // 使用前置聲明的枚舉體
```
如果`Status`的定義被修改,包含這個聲明的頭文件不需要重新編譯。更進一步,如果`Status`被修改(即,增加`audited`枚舉元素),但是`continueProcessing`的行為不受影響(因為`continueProcessing`沒有使用`audited`),`continueProcessing`的實現也不需要重新編譯。
但是如果編譯器需要在枚舉體之前知道它的大小,`C++11`的枚舉體怎么做到可以前置聲明,而`C++98`的枚舉體無法實現?原因是簡單的,對于有作用域的枚舉體的潛在類型是已知的,對于沒有作用域的枚舉體,你可以指定它。
對有作用域的枚舉體,默認的潛在的類型是`int`:
```cpp
enum class Status; // 潛在類型是int
```
如果默認的類型不適用于你,你可重載它:
```cpp
enum class Status: std::uint32_t; // Status潛在類型是
// std::uint32_t
// (來自<cstdint>)
```
無論哪種形式,編譯器都知道有作用域的枚舉體中的枚舉元素的大小。
為了給沒有作用域的枚舉體指定潛在類型,你需要做相同的事情,結果可能是前置聲明:
```cpp
enum Color: std::uint8_t; // 沒有定義域的枚舉體
// 的前置聲明,潛在類型是
// std::uint8_t
```
潛在類型的指定也可以放在枚舉體的定義處:
```cpp
enum class Status: std::uint32_t{ good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
```
從有定義域的枚舉體可以避免命名空間污染和不易受無意義的隱式類型轉換影響的角度看,你聽到至少在一種情形下沒有定義域的枚舉體是有用的可能會感到驚訝。這種情況發生在引用`C++11`的`std::tuples`中的某個域時。例如,假設我們有一個元組,元組中保存著姓名,電子郵件地址,和用戶在社交網站的影響力數值:
```cpp
using UserInfo = // 別名,參見條款9
std::tuple<std::string, // 姓名
std::string, // 電子郵件
std::size_t> ; // 影響力
```
盡管注釋已經說明元組的每部分代表什么意思,但是當你遇到像下面這樣的源代碼時,可能注釋沒有什么用:
```cpp
UserInfo uInfo; // 元組類型的一個對象
...
auto val = std::get<1>(uInfo); // 得到第一個域的值
```
作為一個程序員,你有很多事要做。你真的想去記住元組的第一個域對應的是用戶的電子郵件地址?我不這么認為。使用一個沒有定義域的枚舉體來把名字和域的編號聯系在一起來避免去死記這些東西:
```cpp
enum UserInfoFields {uiName, uiEmail, uiReputation };
UserInfo uInfo; // 和前面一樣
...
auto val = std::get<uiEmail>(uInfo); // 得到電子郵件域的值
```
上面代碼正常工作的原因是`UserInfoFields`到`std::get()`要求的`std::size_t`的隱式類型轉換。
如果使用有作用域的枚舉體的代碼就顯得十分冗余:
```cpp
enum class UserInfoFields { uiName, uiEmail, uiReputaion };
UserInfo uInfo; // 和前面一樣
...
auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
```
寫一個以枚舉元素為參數返回對應的`std::size_t`的類型的值可以減少這種冗余性。`std::get`是一個模板,你提供的值是一個模板參數(注意用的是尖括號,不是圓括號),因此負責將枚舉元素轉化為`std::size_t`的這個函數必須在編譯階段就確定它的結果。就像條款15解釋的,這意味著它必須是一個`constexpr`函數。
實際上,它必須是一個`constexpr`函數模板,因為它應該對任何類型的枚舉體有效。如果我們打算實現這種一般化,我們需要一般化返回值類型。不是返回`std::size_t`,我們需要返回枚舉體的潛在類型。通過`std::underlying_type`類型轉換來實現(關于類型轉換的信息,參見條款9)。最后需要將這個函數聲明為`noexcept`(參見條款14),因為我們知道它永遠不會觸發異常。結果就是這個函數模板可以接受任何的枚舉元素,返回這個元素的在編譯階段的常數值:
```cpp
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`中,`toUType`可以通過將`std::underlying_type<E>::type`替代為`std::underlying_type_t`(參見條款9):
```cpp
template<typename E> // C++14
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
```
更加優雅的`auto`返回值類型(參見條款3)在`C++14`中也是有效的:
```cpp
template<typename E>
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
```
無論寫哪種形式,`toUType`允許我們想下面一樣訪問一個元組的某個域:
```cpp
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
```
這樣依然比使用沒有定義域的枚舉體要復雜,但是它可以避免命名空間污染和不易引起注意的枚舉元素的的類型轉換。很多時候,你可能會決定多敲擊一些額外的鍵盤來避免陷入一個上古時代的枚舉體的技術陷阱中。
|要記住的東西|
|:--------- |
|`C++98`風格的`enum`是沒有作用域的`enum`|
|有作用域的枚舉體的枚舉元素僅僅對枚舉體內部可見。只能通過類型轉換(`cast`)轉換為其他類型|
|有作用域和沒有作用域的`enum`都支持指定潛在類型。有作用域的`enum`的默認潛在類型是`int`。沒有作用域的`enum`沒有默認的潛在類型。|
|有作用域的`enum`總是可以前置聲明的。沒有作用域的`enum`只有當指定潛在類型時才可以前置聲明。|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert