Effective Modern C++ 條款8 用nullptr代替0和NULL

用nullptr代替0和NULL

你看看這樣行不行,0字面上的意思是個int,而不是指針。如果C++代碼中把0用作指針,那麼C++會勉強地把0翻譯爲一個空指針,和一開始說的不太一樣。C++的主要政策是,0是一個int值,而不是指針。

從實踐上來說,NULL也是如此。NULL在細節上有一些不確定性,因爲在實現上允許把NULL附給一個不同於int的整數類型(例如long)。這不常見,但在這裏卻無所謂,因爲我們這裏討論的問題不是NULL的確切類型,而是0和NULL都不是指針類型。


在C++98中,基於上面的問題,使用帶有指針類型或整數類型重載函數的會出乎我們意料。傳遞0或者NULL到重載函數中,指針作爲參數的重載函數不會被調用:

void f(int);     //  f的3個重載函數
void f(bool);
void f(void*);

f(0);  // 調用f(int)
f(NULL);  // 可能不會通過編譯,一般境況下調用f(int)

想要知道f(NULL)的行爲就要求我們知道NULL的實現。如果NULL被定義爲0L(即0是long類型),那麼這次調用是引起歧義的,因爲long轉換成intlong轉換成boollong轉換成void*在編譯器看來是一樣好的。這個函數調用從代碼上看(我要用NULL空指針調用f),和從實際上看(我用某個整數類型調用f)是相互矛盾的。這個違反直覺的行爲使得C++98程序員的指導方針是避免重載函數帶有指針類型和整數類型。這個指導方針在C++11中依然有效,因爲儘管有這條款的建議,但是一些開發者依然使用0和NULL,即使nullptr更好。

nullptr的好處是它不是整數類型,但實話說,它也不是指針類型,但是你可以把它看作一個可以指向所有類型的指針。nullptr的實際類型是std::nullptr_tstd::nullptr_t可以隱式轉換爲所有類型的原生指針,這就使得nullptr像一個可以任何類型的指針。

nullptr調用上面f的重載函數,編譯器會選擇參數爲void*的重載函數,因爲nullptr不能被視爲整數類型:
f(nullptr) // 調用 f(void*)


nullptr可以避免重載選擇的問題,但它不只有這一個優點。它還可以讓代碼更清晰易懂,尤其是當你使用了auto變量。例如,你遇到了以下代碼:

auto result = findRecord( /*arguments*/);

if (result == 0) {
  ...
}

如果你碰巧不知道findRecord函數返回什麼(或者很難查明),那麼你可能不知道result是指針還是整數類型,但你別忘了,0在兩種情況下都是可以繼續運行的。另一方面,如果我們用以下代碼:

auto result = findRecord(/*arguments*/);

if (result == nullptr) {
  ...
}

這就沒有二義性了:result一定是指針類型。


nullptr在涉及模板的時候十分有用。假如你有些函數只能在互斥鎖被鎖的時候纔可以調用,而每個函數又有不同的指針類型:

int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget *pw);

我們像這樣傳遞空指針給函數:

std::mutex f1m, f2m, f3m;  // f1,f2, f3的互斥鎖

using MuGuard = std::lock_guard<std::mutex>;
...
{
  MuGuard g(f1m);
  auto result = f1(0);
}
...
{
  MuGuard g(f2m);
  auto result = f2(NULL);
}
...
{
  MuGuard g(f3m);
  auto result = f3(nullptr);
}

前兩個函數不使用nullptr讓人感到悲傷,但函數是可以運行的,代碼還是有些價值。但是這調用函數的模式——上鎖,調用函數,解鎖——更讓人悲傷,這讓人很煩惱,這種重複的代碼可以設計一個模板來避免:

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)

template <typename FuncType, typename MuxType, typename PtrType>
decltype(auto) 
lockAndCall(FuncType func, MuxType &mutex, PtrType ptr) 
{
  MuxGuard g(mutex);
  return func(ptr);
}

有了lockAndCall這個模板(任意一個版本),調用者可以寫出這樣的代
碼:

auto result1 = lockAndCall(f1, f1m, 0);  // 錯誤
...
auto result2 = lockAndCall(f2, f2m, NULL);  // 錯誤
...
auto result3 = lockAndCall(f3, f3m, nullptr);  // 正確

就像註釋裏所說,前面兩個函數無法通過編譯。第一個函數調用的問題是,當傳0給lockAndCall時,模板類型推斷會把ptr推斷成int類型,因此模板把int類型參數傳給func函數,int類型和f1期待的std::shared_ptr<Widget>類型不兼容,就會出錯。我們傳遞0想表示的是空指針,但是實際變成了傳遞一個普通的int,試圖將int傳遞給接收std::shared_ptr<Widget>的f1是會報錯的。這錯誤主要是因爲在模板中,int的值傳遞給接收std::shared_ptr<Widget>的函數。

第二個函數在本質上錯誤相同。NULL被模板推斷爲整型數類型,那麼參數爲std::unique_ptr<Widget>的f2函數接收到的整數類型的值,所以報錯。

作爲對比,使用nullptr的那個函數調用沒有出現問題。當nullptr傳遞給lockAndCall時,ptr的類型被推斷爲std::nullptr_t,當ptr傳遞給f3時,發生了std::nullptr_tWidget *的隱式轉換,因爲std::nullptr_t可以隱式轉換爲所有類型的指針。

事實上,最迫使你使用nullptr代替0和NULL的原因是,模板類型推斷會爲0和NULL推斷出“錯誤”的類型當你想要一個空指針時。有了nullptr,模板就不會引起什麼問題了。再想想nullptr不受重載函數選擇策略影響,而0和NULL卻容易受到影響。所以,當你要說明空指針時,用nullptr,而不是0或者NULL

總結

需要記住的兩點:

  • 使用nullptr代替0和NULL
  • 避免整數類型與指針類型之間的函數重載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章