簡體   English   中英

使用nullptr有什么好處?

[英]What are the advantages of using nullptr?

這段代碼在概念上對三個指針(安全指針初始化)做了同樣的事情:

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

那么,分配指針nullptr不是為它們賦值NULL0什么好處?

在該代碼中,似乎沒有優勢。 但請考慮以下重載函數:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

將調用哪個函數? 當然,這里的意圖是調用f(char const *) ,但實際上會調用f(int) 那是個大問題1 ,不是嗎?

所以,解決這些問題的方法是使用nullptr

f(nullptr); //first function is called

當然,這不是nullptr的唯一優勢。 這是另一個:

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

因為在模板中, nullptr的類型推導為nullptr_t ,所以你可以這樣寫:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1.在C ++中, NULL定義為#define NULL 0 ,因此它基本上是int ,這就是調用f(int)原因。

C ++ 11引入了nullptr ,它被稱為Null指針常量,它改進了類型安全性解決了模糊情況,這與現有的依賴於實現的空指針常量NULL 能夠理解nullptr的優點。 我們首先需要了解什么是NULL以及與之相關的問題。


什么是NULL

Pre C ++ 11 NULL用於表示沒有值的指針或指向無效的指針。 與流行的概念相反, NULL不是C ++中的關鍵字 它是標准庫頭中定義的標識符。 簡而言之,如果不包含一些標准庫頭,則不能使用NULL 考慮示例程序

int main()
{ 
    int *ptr = NULL;
    return 0;
}

輸出:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

C ++標准將NULL定義為在某些標准庫頭文件中定義的實現定義宏。 NULL的來源來自C和C ++從C繼承.C標准將NULL定義為0(void *)0 但在C ++中有一個微妙的區別。

C ++不能接受這個規范。 與C不同,C ++是一種強類型語言(C不需要從void*到任何類型的顯式轉換,而C ++強制要求顯式轉換)。 這使得C標准指定的NULL定義在許多C ++表達式中無用。 例如:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

如果將NULL定義為(void *)0 ,則上述表達式都不起作用。

  • 情況1:無法編譯,因為從void *std::string需要自動std::string
  • 情況2:無法編譯,因為需要從void *為指向成員函數的指針。

因此,與C不同,C ++標准要求將NULL定義為數字文字00L


那么當我們已經有NULL時,還需要另一個空指針常量嗎?

雖然C ++標准委員會提出了一個適用於C ++的NULL定義,但這個定義有其自身公平的問題。 NULL幾乎適用於所有場景,但並非全部。 對於某些罕見的情況,它給出了令人驚訝和錯誤的結果。 例如

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

輸出:

In Int version

顯然,目的似乎是調用以char*作為參數的版本,但是輸出顯示了調用int版本的函數。 這是因為NULL是一個數字文字。

此外,由於NULL是0或0L是實現定義的,因此在函數重載解析中會有很多混淆。

示例程序:

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

分析上面的片段:

  • 情況1:按預期調用doSomething(char *)
  • 情況2:調用doSomething(int)但可能需要char*版本,因為0 IS也是空指針。
  • 情況3:如果將NULL定義為0 ,則在可能出現doSomething(char *)時調用doSomething(int) ,可能在運行時導致邏輯錯誤。 如果將NULL定義為0L ,則調用不明確並導致編譯錯誤。

因此,根據實現,相同的代碼可以提供各種結果,這顯然是不希望的。 當然,C ++標准委員會想要糾正這一點,這是nullptr的主要動機。


什么是nullptr以及它如何避免NULL的問題?

C ++ 11引入了一個新的關鍵字nullptr作為空指針常量。 與NULL不同,它的行為不是實現定義的。 它不是一個宏,但它有自己的類型。 nullptr的類型為std::nullptr_t C ++ 11適當地定義了nullptr的屬性,以避免NULL的缺點。 總結其屬性:

屬性1:它有自己的類型std::nullptr_t ,和
屬性2:它是可隱式轉換的,可與任何指針類型或指向成員類型的類型相媲美,但是
屬性3:除了bool之外,它不可隱式轉換或與整數類型相比。

請考慮以下示例:

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

在上面的程序中,

  • 案例1:好的 - 財產2
  • 案例2:不好 - 財產3
  • 案例3:好的 - 財產3
  • 案例4:沒有混淆 - 調用char *版本,屬性2和3

因此,引入nullptr避免了舊的NULL的所有問題。

你應該如何以及在哪里使用nullptr

C ++ 11的經驗法則只是在過去使用NULL時開始使用nullptr


標准參考:

C ++ 11標准:C.3.2.4宏NULL
C ++ 11標准:18.2類型
C ++ 11 Standard:4.10指針轉換
C99標准:6.3.2.3指針

這里的真正動機是完美轉發

考慮:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

簡單地說,0是一個特殊 ,但值不能通過系統傳播 - 只有類型可以。 轉發功能是必不可少的,0不能處理它們。 因此,絕對有必要引入nullptr ,其中類型是特殊的,並且類型確實可以傳播。 實際上,MSVC團隊在實施右值引用之后必須提前引入nullptr ,然后才發現這個陷阱。

還有一些其他nullptr情況,其中nullptr可以使生活更輕松 - 但它不是核心案例,因為演員可以解決這些問題。 考慮

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

調用兩個單獨的重載。 另外,考慮一下

void f(int*);
void f(long*);
int main() { f(0); }

這是模棱兩可的。 但是,使用nullptr,您可以提供

void f(std::nullptr_t)
int main() { f(nullptr); }

nullp的基礎知識

std::nullptr_t是空指針文字的類型,nullptr。 它是std::nullptr_t類型的prvalue / rvalue。 存在從nullptr到任何指針類型的空指針值的隱式轉換。

文字0是int,而不是指針。 如果C ++在只能使用指針的上下文中發現自己看0,那么它會勉強將0解釋為空指針,但這是一個后備位置。 C ++的主要策略是0是int,而不是指針。

優點1 - 在指針和整數類型上重載時消除歧義

在C ++ 98中,其主要含義是指針和整數類型的重載可能會導致意外。 將0或NULL傳遞給此類重載從不調用指針重載:

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

關於那個調用的有趣之處在於源代碼的明顯含義(“我用NULL調用有趣的空指針”)和它的實際含義之間的矛盾(“我用某種整數調用有趣 - 而不是null指針”)。

nullptr的優點是它沒有整數類型。 使用nullptr調用重載函數有趣會調用void * overload(即指針重載),因為nullptr不能被視為任何整數:

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

使用nullptr而不是0或NULL可以避免重載決策意外。

當auto用於返回類型時, nullptr優於NULL(0)另一個優點

例如,假設您在代碼庫中遇到此問題:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

如果您碰巧知道(或不能輕易找出)findRecord返回的內容,則可能不清楚結果是指針類型還是整數類型。 畢竟,0(測試的結果是什么)可以是任何一種方式。 另一方面,如果您看到以下內容,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

沒有歧義:結果必須是指針類型。

優勢3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

上面的程序編譯並執行成功但是lockAndCallF1,lockAndCallF2和lockAndCallF3都有冗余代碼。 如果我們可以為所有這些lockAndCallF1, lockAndCallF2 & lockAndCallF3編寫模板,那么編寫這樣的代碼是很遺憾的。 所以它可以用模板推廣。 我編寫了模板函數lockAndCall而不是多個定義lockAndCallF1, lockAndCallF2 & lockAndCallF3用於冗余代碼。

代碼重新計算如下:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

詳細分析為什么lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)編譯失敗而不是lockAndCall(f3, f3m, nullptr)

為什么編譯lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)失敗?

問題是,當0傳遞給lockAndCall時,模板類型推導開始計算其類型。 0的類型是int,因此這是對lockAndCall調用實例化中的參數ptr的類型。 不幸的是,這意味着在lockAndCall內部調用func時,正在傳遞一個int,並且它與f1期望的std::shared_ptr<int>參數不兼容。 在調用lockAndCall時傳遞的0用於表示空指針,但實際傳遞的是int。 嘗試將此int作為std::shared_ptr<int>傳遞給f1是一個類型錯誤。 lockAndCall的調用失敗,因為在模板內部,int被傳遞給需要std::shared_ptr<int>的函數。

涉及NULL的調用的分析基本相同。 NULL傳遞給lockAndCall ,會為參數ptr推導出一個整數類型,當ptr -an int或int-like lockAndCall傳遞給f2時會發生類型錯誤, f2期望得到一個std::unique_ptr<int>

相反,涉及nullptr的調用沒有問題。 nullptr傳遞給lockAndCallptr的類型推斷為std::nullptr_t ptr傳遞給f3 ,存在從std::nullptr_tint*的隱式轉換,因為std::nullptr_t隱式轉換為所有指針類型。

建議,每當要引用空指針時,請使用nullptr,而不是0或NULL

以您顯示示例的方式使用nullptr沒有直接的優勢。
但考慮一下你有2個同名函數的情況; 1取int和另一個int*

void foo(int);
void foo(int*);

如果你想通過傳遞NULL來調用foo(int*) ,那么方法是:

foo((int*)0); // note: foo(NULL) means foo(0)

nullptr使它更簡單直觀

foo(nullptr);

Bjarne網頁的其他鏈接
不相關但在C ++ 11方面注意:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

正如其他人已經說過的那樣,它的主要優勢在於超載。 雖然顯式的int與指針重載很少見,但考慮標准的庫函數,比如std::fill (它在C ++ 03中多次咬過我):

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

不編譯: Cannot convert int to MyClass*

IMO比那些超載問題更重要:在深層嵌套的模板結構中,很難不忘記類型,並且給出明確的簽名是一項非常努力的工作。 因此,對於您使用的所有內容,越精確地集中於預期目的,就越好,它將減少對顯式簽名的需求,並允許編譯器在出現問題時生成更具洞察力的錯誤消息。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM