Item 8: 比起0和NULL更偏愛nullptr

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

博客已經遷移到這裏啦

先讓我們看一些概念:字面上的0是一個int,不是一個指針。如果C++發現0在上下文中只能被用作指針,它會勉強把0解釋爲一個null指針,但這只是一個應變的方案。C++的主要規則還是把0視爲int,而不是一個指針。

實際上,NULL也是這樣的。NULL的情況,在細節方面有一些不確定性,因爲C++標準允許它的實現不管是不是int只需要給出一個數值類型(比如,long)。雖然這和0不一樣,但是事實上沒有關係,因爲這裏的問題不是NULL的具體類型,而是0和NULL都不是指針類型。

在C++98中,這個問題造成的最主要的影響就是指針和數值類型的重載會導致意外情況。傳入一個0或者NULL給這樣的重載,則永遠不會調用指針版本的重載:

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(也就是long類型的0),那麼對函數的調用引起歧義,因爲把long轉換爲int,bool,void*都是同樣可行的。關於調用f的一個有趣的事情是源代碼表面上的意義(我使用NULL—null指針,調用f)和它實際的意義(我使用某種整形—不是一個指針,調用f)之間的矛盾。這種反直覺的行爲導致了C++98程序員指導方針讓我們避免重載指針類型和整形類型。這個方針在C++11中仍然有效,因爲,就算有這條Item的建議,儘管nullptr是更好的選擇,有些開發者還是會繼續使用0和NULL。

nullptr的優點不只是它不是一個整形類型。老實說,它也不是一個指針類型,但是你能把它想成是一個指向所有類型的指針。nullptr的實際類型是std::nullptr_t,在一個完美的循環定義(circular definition)中,std::nullptr_t被定義爲nullptr的類型。std::nullptr_t類型能隱式轉換到所有的原始(raw)指針類型,就是這個原因,讓nullptr表現得像它是所有類型的指針。

用nullptr調用f的重載函數,實際會調用void*版本的函數(也就是指針版本的重載),因爲nullptr不能被視爲任何數值類型:

f(nullptr);         //調用f(void*)版本的重載函數

因此使用nullptr來代替0和NULL避免了意外的重載解析,但是這不是他唯一的優點。它還能提升代碼的可讀性,尤其是在涉及auto變量時。舉個例子,假設你遇到下面的代碼:

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

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

如果你碰巧不知道(或者很難找出)findRecord返回的類型,這裏對於result是指針類型還是整形類型就不明確了。總之,0(用來和result進行比較)可以是兩個中的任意一個。但是,如果你看到下面的代碼:

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

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

這裏將沒有歧義:result肯定是一個指針類型。

當template涉及後,nullptr將變得更加耀眼。假設你有一些函數,它們只能在適當的互斥量被鎖住之後才能調用。每個函數使用不同類型的指針:

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

想要傳入null指針來調用函數的代碼看起來像這樣:

std::mutex f1m, f2m, f3m;

using MuxGuard =                //C++11 的typedef,看Item 9
    std::lock_guard<std::mutex>;
...

{
    MuGuard g(f1m);             //鎖住f1的互斥量
    auto result = f1(0);        //傳入0作爲null指針給f1
}                               //解鎖互斥量

...

{
    MuGuard g(f2m);             //鎖住f2的互斥量
    auto result = f2(NULL);     //傳入NULL作爲null指針給f2
}                               //解鎖互斥量

...

{
    MuGuard g(f3m);             //鎖住f3的互斥量
    auto result = f3(nullptr);  //傳入nullptr作爲null指針給f3
}                               //解鎖互斥量 

不能用nullptr調用前面兩個函數是不幸的,但是代碼能正常工作,這一點很重要。然而,代碼中重複的模式(加鎖,調用函數,解鎖)不僅不幸。這更是令人不安的。這種源代碼的重複問題就是template被設計來解決的其中一種,所以讓我們模板化這個模式:

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)))轟擊了你的大腦,請問你的大腦經歷並贊成過Item 3(解釋了發生了什麼)嗎?你也能看到C++14版本的代碼,返回類型能被簡化成一個簡單的decltype(auto):

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

給出lockAndCalltemplate(每個版本)的調用函數,能這麼寫:

auto result1 = lockAndCall(f1, f1m, 0);         //錯誤

...

auto result2 = lockAndCall(f2, f2m, NULL);      //錯誤

...

auto result3 = lockAndCall(f3, f3m, nullptr);   //正確

很好,它們能這麼寫,但是,正如註釋說的,前面兩種情況,代碼無法通過編譯。第一種調用情況的問題是當0被傳入lockAndCall時,template類型推導會找出它的類型。0的類型過去是,現在是,未來也永遠是int,所以這就是在實例化的lockAndCall中,ptr參數的類型。不幸地,這意味着在lockAndCall中,調用func時,會傳入一個int類型,然而它和f1需要的std::shared_patr參數不兼容。在lockAndCall調用中,嘗試用傳入0來代替一個null指針,但是實際上傳入的是普通的int。嘗試傳入一個int作爲std::shared_ptr給f1將產生類型錯誤。對於用0調用lockAndCall會失敗,是因爲在template中,一個int被傳入一個需要std::shared_ptr的函數。(譯註:理解起來很簡單,這裏沒有int 到 指針類型的轉換,只有0視爲指針這一情況。所以調用會失敗。)

對於調用涉及NULL的分析在本質上和0是一樣的。當NULL被傳入lockAndCall時,一個整形類型被推導爲ptr參數的類型,並且當一個int或一個類int類型的參數被傳入f2(需要一個std::unique_ptr類型的參數)時,類型錯誤就發生了。

作爲對比,當調用涉及nullptr時沒有問題。當nullptr被傳入lockAndCall時,ptr的類型被推導爲std::nullptr_t。當ptr被傳入f3時,這裏發生了一個從std::nullptr_t到Widget*的隱式轉換,因爲std::nullptr_t能隱式轉換到任何類型的指針。

事實上,當你想使用一個null指針時,對於0和NULL,template類型推導推導出“錯誤”的類型
(也就是,它們的“對的”類型本應該是代表着一個null指針的),這是用nullptr代替0或NULL最重要的原因。使用nullptr,template不會面臨特殊的挑戰。結合nullptr不會遭受意外的重載解析(0和NULL很可能遭受)的事實,情況很明瞭了。當你想使用一個null指針的時候,使用nullptr,而不是0或NULL。

            你要記住的事
  • 比起0和NULL更偏愛nullptr

  • 避免重載整形類型和指針類型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章