nullptrを使用する利点は何ですか?


163

このコードは、概念的には3つのポインターに対して同じことを行います(安全なポインター初期化)。

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

だから、ポインタを割り当てることの利点どのようなものnullptrそれらに値を代入上NULLかは0


39
一つには、服用オーバーロードされた関数intとはvoid *選択しません。intオーバーバージョンvoid *使用してバージョンをnullptr
chris

2
まあf(nullptr)とは違うですf(NULL)。ただし、上記のコード(ローカル変数への割り当て)に関する限り、3つのポインターはすべてまったく同じです。唯一の利点はコードの読みやすさです。
balki

2
これをFAQ、Prasoonにすることに賛成です。ありがとう!
sbi

1
NB NULLは歴史的に0であることが保証されていませんが、C99と同じです。バイトが必ずしも8ビット長である必要はなく、trueとfalseはアーキテクチャに依存する値でした。この質問は、nullptr0とNULL
awiebe

回答:


180

そのコードでは、利点はないようです。ただし、次のオーバーロードされた関数を検討してください。

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ため、基本的にはなのでintf(int)が呼び出されます。


1
Mehrdadが述べたように、これらの種類のオーバーロードはかなりまれです。他に関連する利点はありnullptrますか?(いいえ、私は要求していません)
マークガルシア

2
@MarkGarciaは、これが役に立つかもしれません:stackoverflow.com/questions/13665349/...
クリス

9
あなたの脚注は逆のようです。NULL標準では整数型が必要であり、そのため、通常は0またはとして定義され0Lます。またnullptr_t、このオーバーロードは、のような別のタイプのnullポインターではなく、での呼び出しのみをキャッチするため、私がそのオーバーロードが好きかどうかもわかりません。しかし、いくつかの用途があると私は信じています。たとえそれがすべて、「なし」を意味する独自の単一値のプレースホルダータイプを定義することを節約するだけの場合でもです。nullptr(void*)0
スティーブジェソップ

1
別の利点(確かにマイナーですが)にnullptrは、明確に定義された数値があるのに対し、nullポインター定数にはないことです。NULLポインター定数は、その型(それが何であれ)のNULLポインターに変換されます。同じ型の2つのnullポインターが同じように比較する必要があり、ブール変換によりnullポインターがに変換されfalseます。他には何も必要ありません。したがって、コンパイラーは(愚かですが、可能性があります)0xabcdef1234nullポインターにeg またはその他の数値を使用できます。一方、nullptr数値ゼロに変換する必要があります。
デイモン

1
@DeadMG:私の答えの何が間違っていますか?それf(nullptr)は意図した関数を呼び出さないでしょうか?動機は複数ありました。他の多くの有用なものがプログラマー自身によって今後数年で発見される可能性があります。だから、そこにあると言うことができない唯一の真の使用nullptr
Nawaz

87

C ++ 11が導入しましたnullptr。これはNullポインター定数として知られており、既存の実装に依存するnullポインター定数とは異なり、型の安全性向上させあいまいな状況解決しますNULL。の利点を理解できるようにするためnullptr。まずNULL、それに関連する問題とは何かを理解する必要があります。


NULL正確には何ですか?

C ++ 11より前のバージョンNULLは、値のないポインターまたは有効なものを指し示さないポインターを表すために使用されていました。一般的な概念に反して、NULLC ++ではキーワードではありません。これは、標準ライブラリヘッダーで定義されている識別子です。つまり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
  • ケース2:void *メンバー関数へのポインターへのキャストが必要なため、コンパイルされません。

したがって、Cとは異なり、C ++標準ではNULLを数値リテラル0またはとして定義することが義務付けられています0L


それで、NULLすでにあるときに別の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*0ISもnullポインターであるため、バージョンが必要な場合があります。
  • ケース3:もしがNULLのように定義され0、通話doSomething(int)多分doSomething(char *)、おそらく、実行時に論理エラーが生じ意図されていました、。場合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: OK-プロパティ2
  • ケース2: OKではない-プロパティ3
  • ケース3: OK-プロパティ3
  • ケース4:混乱なし- char *バージョン、プロパティ2および3を呼び出す

したがって、nullptrの導入により、古き良きNULLのすべての問題が回避されます。

どのように、どこで使用する必要がありますnullptrか?

C ++ 11の経験則nullptrは、以前はNULLを使用していなかった場合に使用を開始することです。


標準参照:

C ++ 11標準:C.3.2.4マクロNULL
C ++ 11標準:18.2タイプ
C ++ 11標準:4.10ポインター変換
C99標準:6.3.2.3ポインター


私がnullptr知っているので、私はすでにあなたの最後のアドバイスを実践していますが、それが実際にコードにどのような違いがあるのか​​はわかりませんでした。すばらしい回答、特に努力に感謝します。そのトピックについて私にかなりの光をもたらした。
Mark Garcia、

「特定の標準ライブラリヘッダーファイル。」->最初から「cstddef」と書いてみませんか?
mxmlnkn

nullptrをブール型に変換できるようにする必要があるのはなぜですか?もう少し詳しく説明してもらえますか?
Robert Wang

...値のないポインタを表すために使用されていました...変数には常に値があります。それはノイズかもしれません0xccccc....が、値のない変数は本質的に矛盾しています。
3Dave 2016

「ケース3:OK-プロパティ3」(行bool flag = nullptr;)。いや、ないOK、私はG ++ 6でコンパイル時に次のエラーを取得する:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
ゲオルク

23

ここでの真の動機は、完璧な転送です。

考慮してください:

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人生を楽にすることができる他のいくつかのコーナーケースがありますが、キャストがこれらの問題を解決できるので、コアケースではありません。検討する

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

2つの個別のオーバーロードを呼び出します。さらに、考慮してください

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

これはあいまいです。しかし、nullptrを使用すると、

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

7
おかしい。その答えの半分は、あなたに従っている他の2つの答えと同じである「非常に間違った」答え!!!
Nawaz

転送の問題はキャストでも解決できます。forward((int*)0)動作します。何か不足していますか?
jcsahnwaldtがモニカを復活させる

5

nullptrの基本

std::nullptr_tnullポインタリテラルnullptrの型です。タイプのprvalue / rvalue std::nullptr_tです。nullptrから任意のポインター型のnullポインター値への暗黙的な変換が存在します。

リテラル0は、ポインターではなくintです。C ++が、ポインターのみを使用できるコンテキストで0を見ていることに気付いた場合、それは0をnullポインターとして不当に解釈しますが、それはフォールバック位置です。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ポインタでfunを呼び出しています」)と実際の意味(「nullではなく、何らかの整数でfunを呼び出しています。ポインタ」)。

nullptrの利点は、整数型がないことです。nullptrは整数と見なすことができないため、nullptrでオーバーロードされた関数funを呼び出すと、void *オーバーロード(つまり、ポインターオーバーロード)が呼び出されます。

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

したがって、0またはNULLの代わりにnullptrを使用すると、オーバーロードの解決の驚きを回避できます。

戻り値の型にautoを使用nullptrするNULL(0)場合のもう1つの利点

たとえば、コードベースでこれが発生したとします。

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)失敗したのですか?

問題は、lockAndCallに0が渡されると、テンプレートタイプの推論が始まり、そのタイプを理解することです。タイプ0はintなので、lockAndCallへのこの呼び出しのインスタンス化内のパラメーターptrのタイプです。残念ながら、これは、lockAndCall内のfuncの呼び出しで、intが渡されていることを意味しstd::shared_ptr<int>ますf1。これは、予期するパラメーターと互換性がありません。への呼び出しで渡された0はlockAndCall、nullポインタを表すためのものでしたが、実際に渡されたのはintでした。このintをstd::shared_ptr<int>タイプエラーとしてf1に渡そうとすると、lockAndCallテンプレート内で、intがを必要とする関数に渡されているため、with 0 の呼び出しは失敗しますstd::shared_ptr<int>

関係する呼び出しの分析NULLは基本的に同じです。ときNULLに渡されたlockAndCall、一体型は、パラメータPTRのために推定され、時に型エラーが発生しptr-an intまたはint型のように渡されたタイプが-されf2得ることを期待され、std::unique_ptr<int>

対照的に、通話は問題nullptrありません。ときnullptrに渡されlockAndCall、用型がptrあることを推測されますstd::nullptr_t。ときptrに渡されるf3、からの暗黙の型変換がありますstd::nullptr_tにはint*あるため、std::nullptr_tすべてのポインタ型に暗黙的に変換します。

ヌルポインターを参照する場合は常に、0やではなくnullptrを使用することをお勧めしますNULL


4

nullptrあなたが例を示した方法で持っていることの直接的な利点はありません。
しかし、同じ名前の2つの関数がある状況を考えてみましょう。1テイクintともう1テイクint*

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

foo(int*)NULLを渡して呼び出す場合、方法は次のとおりです。

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

nullptrより簡単で直感的になります

foo(nullptr);

BjarneのWebページからの追加リンク
無関係ですが、C ++ 11に関する注意事項:

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

3
参考までに、decltype(nullptr)ですstd::nullptr_t
クリス

2
@MarkGarcia、私が知る限り、それは本格的なタイプです。
chris

5
@MarkGarcia、それは興味深い質問です。cppreferenceには:がありtypedef decltype(nullptr) nullptr_t;ます。私は標準を見ることができると思います。ああ、見つけました:注:std :: nullptr_tは、ポインター型でもメンバー型へのポインターでもない特殊な型です。むしろ、このタイプのprvalueはnullポインター定数であり、nullポインター値またはnullメンバーポインター値に変換できます。
chris

2
@DeadMG:動機は複数ありました。他の多くの有用なものがプログラマー自身によって今後数年で発見される可能性があります。だから、そこにあると言うことができない唯一の真の使用nullptr
Nawaz

2
@DeadMG:しかし、あなたが答えで話していた「真の動機」について語っていないという理由だけで、この答えは「まったく正しくない」と述べました。この回答(および私の回答)があなたから反対票を受け取っただけではありません。
Nawaz

4

他の人がすでに言ったように、その主な利点は過負荷にあります。また、明示的なintオーバーロードとポインターのオーバーロードはまれである可能性がありますが、次のような標準ライブラリ関数を検討しstd::fillてください(C ++ 03で何度も私を悩ませています)。

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

コンパイルしません:Cannot convert int to MyClass*


2

これらのオーバーロードの問題よりもIMOの方が重要です。深くネストされたテンプレート構造では、タイプを追跡できなくなることは難しく、明示的なシグネチャを与えることはかなりの努力です。したがって、使用するすべてのものについて、意図した目的により正確に焦点を合わせるほど、明示的な署名の必要性が減り、何か問題が発生したときにコンパイラがより洞察に富んだエラーメッセージを生成できるようになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.