條款五:優先使用`nullptr`而不是`0`或者`NULL`
=========================
`0`字面上是一個`int`類型,而不是指針,這是顯而易見的。`C++`掃描到一個`0`,但是發現在上下文中僅有一個指針用到了它,編譯器將勉強將`0`解釋為空指針,但是這僅僅是一個應變之策。`C++`最初始的原則是`0`是`int`而非指針。
經驗上講,同樣的情況對`NULL`也是存在的。對`NULL`而言,仍有一些細節上的不確定性,因為賦予`NULL`一個除了`int`(即`long`)以外的整數類型是被允許的。這不常見,但是這真的是沒有問題的,因為此處的焦點不是`NULL`的確切類型而是`0`和`NULL`都不屬于指針類型。
在`C++98`中,這意味著重載指針和整數類型的函數的行為會令人吃驚。傳遞`0`或者`NULL`作為參數給重載函數永遠不會調用指針重載的那個函數:
```cpp
void f(int); // 函數f的三個重載
void f(bool);
void f(void*);
f(0); // 調用 f(int),而非f(void*)
f(NULL); // 可能無法編譯,但是調用f(int)
// 不可能調用 f(void*)
```
`f(NULL)`行為的不確定性的確反映了在實現`NULL`的類型上存在的自由發揮空間。如果`NULL`被定為`0L`(即`0`作為一個`long`整形),函數的調用是有歧義的,因為`long`轉化為`int`,`long`轉化為`bool`,`0L`轉換為`void*`都被認為是同樣可行的。關于這個函數調用有意思的事情是在源代碼的字面意思(使用`NULL`調用`f`,`NULL`應該是個空指針)和它的真實意義(一個整數在調用`f`,`NULL`不是空指針)存在著沖突。這種違背直覺的行為正是`C++98`程序員不被允許重載指針和整數類型的原因。這個原則對于`C++11`依然有效,因為盡管有本條款的力薦,仍然還有一些開發者繼續使用`0`和`NULL`,雖然`nullptr`是一個更好的選擇。
`nullptr`的優勢是它不再是一個整數類型。誠實的講,它也不是一個指針類型,但是你可以把它想象成一個可以指向任意類型的指針。`nullptr`的類型實際上是`std::nullptr_t`,`std::nullptr_t`定義為`nullptr`的類型,這是一個完美的循環定義。`std::nullptr_t`可以隱式的轉換為所有的原始的指針類型,這使得`nullptr`表現的像可以指向任意類型的指針。
使用`nullptr`作為參數去調用重載函數`f`將會調用`f(void*)`重載體,因為`nullptr`不能被視為整數類型的:
```cpp
f(nullptr); //調用f(void*)重載體
```
使用`nullptr`而不是`0`或者`NULL`,可以避免重載解析上的令人吃驚行為,但是它的優勢不僅限于此。它可以提高代碼的清晰度,尤其是牽扯到`auto`類型變量的時候。例如,你在一個代碼庫中遇到下面代碼:
```cpp
auto result = findRecord( /* arguments */);
if(result == 0){
...
}
```
如果你不能輕松地的看出`findRecord`返回的是什么,要知道`result`是一個指針還是整數類型并不是很簡單的。畢竟,`0`(被用來測試`result`的)即可以當做指針也可以當做整數類型。另一方面,你如果看到下面的代碼:
```cpp
auto result = findRecord( /* arguments */);
if(reuslt == nullptr){
...
}
```
明顯就沒有歧義了:`result`一定是個指針類型。
當模板進入我們考慮的范圍,`nullptr`的光芒則顯得更加耀眼了。假想你有一些函數,只有當對應的互斥量被鎖定的時候,這些函數才可以被調用。每個函數的參數是不同類型的指針:
```cpp
int f1(std::shared_ptr<Widget> spw); // 只有對應的
double f2(std::unique_ptr<Widget> upw); // 互斥量被鎖定
bool f3(Widget* pw); // 才會調用這些函數
```
想傳遞空指針給這些函數的調用看上去像這樣:
```cpp
std::mutex f1m, f2m, f3m; // 對應于f1, f2和f3的互斥量
using MuxGuard = // C++11 版typedef;參加條款9
std::lock_guard<std::mutex>;
...
{
MuxGuard g(f1m); // 為f1鎖定互斥量
auto result = f1(0); // 將0當做空指針作為參數傳給f1
} // 解鎖互斥量
...
{
MuxGuard g(f2m); // 為f2鎖定互斥量
auto result = f2(NULL); // 將NULL當做空指針作為參數傳給f2
} // 解鎖互斥量
...
{
MuxGuard g(f3m); // 為f3鎖定互斥量
auto result = f3(nullptr); // 將nullptr當做空指針作為參數傳給f3
} // 解鎖互斥量
```
在前兩個函數調用中沒有使用`nullptr`是令人沮喪的,但是上面的代碼是可以工作的,這才是最重要的。然而,代碼中的重復模式——鎖定互斥量,調用函數,解鎖互斥量——才是更令人沮喪和反感的。避免這種重復風格的代碼正是模板的設計初衷,因此,讓我們使用模板化上面的模式:
```cpp
template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) -> decltype(func(ptr))
{
MuxGuard g(mutex);
return func(ptr);
}
```
如果這個函數的返回值類型(`auto ...->decltype(func(ptr))`)讓你撓頭不已,你應該到條款3尋求一下幫助,在那里我們已經做過詳細的介紹。在`C++14`中,你可以看到,返回值可以通過簡單的`decltype(auto)`推導得出:
```cpp
template<typename FuncType,
typename MuxType,
typename PtrType>
decltype(auto) lockAndCall(FuncType func, // C++14
MuxType& mutex,
PtrType ptr)
{
MuxGuard g(mutex);
return func(ptr);
}
```
給定`lockAndCall`模板(上邊的任意版本),調用者可以寫像下面的代碼:
```cpp
auto result1 = lockAndCall(f1, f1m, 0); // 錯誤
...
auto result2 = lockAndCall(f2, f2m, NULL); // 錯誤
...
auto result3 = lockAndCall(f3, f2m, nullptr); // 正確
```
他們可以這樣寫,但是就如注釋中指明的,三種情況里面的兩種是無法編譯通過。在第一個調用中,當把`0`作為參數傳給`lockAndCall`,模板通過類型推導得知它的類型。`0`的類型總是`int`,這就是對`lockAndCall`的調用實例化的時候的類型。不幸的是,這意味著在`lockAndCall`中調用`func`,被傳入的是`int`,這個`f1`期望接受的參數`std::share_ptr<Widget>`是不不兼容的。傳入到`lockAndCall`的`0`嘗試來表示一個空指針,但是正真不傳入的是一個普通的`int`類型。嘗試將`int`作為`std::share_ptr<Widget>`傳給`f1`會導致一個類型沖突錯誤。使用`0`調用`lockAndCall`會失敗,因為在模板中,一個`int`類型傳給一個要求參數是`std::share_ptr<Widget>`的函數。
對調用`NULL`的情況的分析基本上是一樣的。當`NULL`傳遞給`lockAndCall`時,從參數`ptr`推導出的類型是整數類型,當`ptr`——一個`int`或者類`int`的類型——傳給`f2`,一個類型錯誤將會發生,因為這個函數期待的是得到一個`std::unique_ptr<Widget>`類型的參數。
相反,使用`nullptr`是沒有問題的。當`nullptr`傳遞給`lockAndCall`,`ptr`的類型被推導為`std::nullptr_t`。當`ptr`被傳遞給`f3`,有一個由`std::nullptr_t`到`Widget*`的隱形轉換,因為`std::nullptr_t`可以隱式轉換為任何類型的指針。
真正的原因是,對于`0`和`NULL`,模板類型推導出了錯誤的類型(他們的真正類型,而不是它們作為空指針而體現出的退化的內涵),這是在需要用到空指針時使用`nullptr`而非`0`或者`NULL`最引人注目的原因。使用`nullptr`,模板不會造成額外的困擾。另外結合`nullptr`在重載中不會導致像`0`和`NULL`那樣的詭異行為的事實,勝負已定。當你需要用到空指針時,使用`nullptr`而不是`0`或者`NULL`。
|要記住的東西|
|:--------- |
|相較于`0`和`NULL`,優先使用`nullptr`|
|避免整數類型和指針類型之間的重載|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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