1. 程式人生 > >優先選擇nullptr而不是0和NULL

優先選擇nullptr而不是0和NULL

base 錯誤 fun ror hellip 的確 mage 如果 template

我們知道:0是一個int,而不是一個指針。如果C++在一個只有指針才能夠使用的上下文中發現它只有一個0,那麽它會勉強將0解釋成空指針,但那時一種倒退行為。C++的主要方針是0就是一個int,而不是指針。

實際上來說,對於NULL也是一樣。關於NULL還有一些不確定因素,因為其實現允許給NULL一個整型而不必是int(比如說long)。這並不常見,但是也無關緊要,因為這裏的問題並不是NULL的確切類型是什麽,而是0和NULL都不是指針類型。

在C++98中,主要的啟示就是對指針和整型分別進行重載可能會導致意想不到的結果。傳遞0或者NULL給這些重載將永遠不會調用指針版本的重載函數:

void f(int
); // three overloads of f void f(bool); void f(void*); f(0); // calls f(int), not f(void*) f(NULL); // might not compile, but typically calls // f(int). Never calls f(void*)

關於f(NULL)行為的不確定性是NULL的實現類型不確定性的一個反映。如果NULL被定義成,比如說,0L(也就是說是long),那麽該調用就很模棱兩可,因為long到int的轉換,long到bool的轉換以及0L到void*的轉換被認為一樣好。關於該調用的一個有趣的事就是該源代碼看起來的含義(”我用NULL(空指針)調用f”)和其實際的含義(”我用NULL(某種整數)調用f”)之間的沖突。這種違法直覺的行為促成了c++98程序員的一個指導方針就是避免重載指針類型和整型。該指導方針在C++11中依然有效,因為盡管我在這裏建議大家用nullptr,但是很多開發者很可能還是繼續使用0和NULL。

nullptr的優勢在於它並不是一個整型。實話實說,它其實也不是一個指針類型,但是你可以認為它是所有類型的指針。nullptr的真實類型是std::nullptr_t,而std::nullptr_t又被定義成nullptr的類型,形成一個極好的環形定義。std::nullptr_t可以隱形轉換任何原始指針類型,而正是這才讓nullptr用起來像是所有類型的指針。

使用nullptr來調用重載函數f會調用void*版本的重載函數,因為nullptr不能被當做成一個整型來看:

f(nullptr);         //calls f(void*) overload

所以使用nullptr而不是0或者NULL防止調用不合理的重載函數,但是這並不是其僅有的優勢。它還可以提高代碼的清晰度,尤其當你使用auto變量時。例如,假設你遇到下面這種代碼:

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

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

技術分享

如果你恰好不知道(或者不容易弄清楚)findRecord的返回類型,那麽這個result是一個指針類型還是一個整型就可能不太清楚了。畢竟,0對於上面哪一種情況都適用。如果你看到下面的代碼:

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

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

就沒有任何歧義了:result就是一個指針類型

而當涉及到模板時,nullptr更是大顯光彩。假設你有一些函數,只有當合適的mutex被鎖定時才應該被調用。每個函數都接受一個不同類型的指針:

int f1(std::shared_ptr<Widget> spw);        // call these only when
double f2(std::unique_ptr<Widget> upw);     // the appropriate
bool f3(Widget* pw);                        // mutex is locked

想要傳遞空指針的函數調用寫起來可能像下面這樣:

std::mutex f1m, f2m, f3m;   //mutexes for f1,f2,f3

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

{
    MuxGuard g(f1m);          //lock mutex for f1
    auto result = f1(0);      //pass 0 as null ptr to f1
}                             //unlock mutex

{
    MuxGuard g(f2m);          //lock mutex for f2
    auto result = f2(NULL);   //pass NULL as null ptr to f2
}                             //unlock mutex

{
    MuxGuard g(f3m);          //lock mutex for f3
    auto result = f3(nullptr);//pass nullptr as null ptr to f3
}                             //unlock mutex


該代碼中前兩個調用沒有使用nullptr真是一個悲哀,但是這份代碼可以正常工作,有一定價值。然而,調用代碼中的重復模式—鎖mutex,調用函數,解鎖—卻更加讓人悲傷。這很令人煩。這種類型的代碼重復是模板被設計用來避免的事物之一,所以咱們對這種模式使用模板:

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>
auto lockAndCall(FuncType func,            //C++14
                 MuxType& mutex,
                 PtrType ptr) 
{
    MuxGuard g(mutex);
    return func(ptr);
}

基於lockAndCall模板(哪一個版本都行),調用代碼可以寫成如下:

auto result1 = lockAndCall(f1,f1m,0);  //error!
...
auto result2 = lockAndCall(f2,f2m,NULL); //error!
...
auto result3 = lockAndCall(f3,f3m,nullptr); //fine

他們可以這樣寫,但是正如評論所說的,前兩個並不會通過編譯。第一個調用的問題在於0被傳遞給lockAndCall,模板類型推斷計算出它的類型。0的類型,過去是,現在也一直都是int,所以在針對這個調用的lockAndCall實例中,其參數ptr的類型就是int。不幸的是,這意味著lockAndCall內部的func調用中,一個int被傳遞了,而這和f1期待的std::shared_ptr<Widget>參數類型不匹配。傳遞給lockAndCall的0本來打算是表示空指針的,但是傳遞進去的實際類型是一個普通的int。嘗試給f1傳遞int當做其std::shared<Widget>參數會導致類型錯誤。使用0來調用lockAndCall會失敗是因為在模板內部,一個int類型被傳遞給了一個需要std::shared_ptr<Widget>類型的函數裏。

對於涉及到NULL的調用的分析和上面基本上一模一樣。當NULL被傳遞給lockAndCall時,對參數ptr推斷出的類型是一個整型,而當一個int或者類似於int的類型被傳遞個期待一個std::unique+ptr<Widget>f2時,就會發生類型錯誤。

作為對比,使用nullptr調用就不存在問題。當nullptr被傳遞給lockAndCall時,ptr的類型被推斷成std::nullptr_t。當ptr被傳遞給f3時,有一個隱性轉換將std::nullptr_t轉換成Widget*類型,因為std::nullptr_t可以隱性轉換成任意指針類型。

當你想要表示一個空指針時,使用nullptr而不是0或者NULL的最大一個原因就在於模板類型推斷會給0和NULL推斷出”錯誤的”類型(也就是說,它們的真實類型,而不是它們退化的含義,表示一個空指針)。使用nullptr,模板就不會出現什麽問題。另外,nullptr不會導致像0和NULL那樣的重載決議異常,所以,當你想要表示一個空指針時,使用nullptr,別使用0和NULL.

要點記憶

  • 優先選擇nullptr而不是0和NULL
  • 避免對整型和指針類型進行重載

優先選擇nullptr而不是0和NULL