このコードは、概念的には 3 つのポインターに対して同じことを行います (安全なポインターの初期化)。
int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;
nullptr
では、ポインタに値を割り当てるよりも、ポインタを割り当てる利点は何NULL
ですか0
?
そのコードでは、利点はないようです。ただし、次のオーバーロードされた関数を検討してください。
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
NULL
nullptr
NULL
NULL
正確には何ですか?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
orとして定義しまし(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
、上記の式はどちらも機能しません。
void *
からへの自動キャストが必要なため、コンパイルされませんstd::string
。 void *
メンバー関数へのポインターへの キャストが必要なため、コンパイルされません。そのため、C とは異なり、C++ 標準では NULL を数値リテラル0
orとして定義することが義務付けられています0L
。
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
}
上記のスニペットを分析すると:
doSomething(char *)
。 doSomething(int)
が、 IS もヌル ポインターである char*
ため、バージョンが必要な場合があります。0
NULL
が として定義されている場合、おそらく意図したとき0
に呼び出し、実行時に論理エラーが発生する可能性があります。が として定義されている場合、呼び出しがあいまいで、コンパイル エラーが発生します。doSomething(int)
doSomething(char *)
NULL
0L
したがって、実装によっては、同じコードがさまざまな結果をもたらす可能性があり、これは明らかに望ましくありません。当然のことながら、C++ 標準委員会はこれを修正したいと考えており、それが nullptr の主な動機です。
nullptr
、どのように問題を回避するのNULL
でしょうか?nullptr
C++11 では、 null ポインター定数として機能する新しいキーワードが導入されています。NULL とは異なり、その動作は実装定義ではありません。これはマクロではありませんが、独自のタイプを持っています。nullptr の型はstd::nullptr_t
です。C++11 は、NULL の欠点を回避するために、nullptr のプロパティを適切に定義します。その特性を要約すると、次のようになります。
プロパティ 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;
}
上記のプログラムでは、
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 ポインター
ここでの本当の動機は完全転送です。
検討:
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); }
nullptr の基本
std::nullptr_t
null ポインタリテラル nullptr の型です。タイプの prvalue/rvaluestd::nullptr_t
です。nullptr から任意のポインター型の null ポインター値への暗黙的な変換が存在します。
リテラル 0 は int であり、ポインターではありません。ポインターしか使用できないコンテキストで C++ が 0 を参照していることに気付いた場合、C++ はしぶしぶ 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 ポインターで fun を呼び出しています」) と実際の意味 (「null ではなく何らかの整数で fun を呼び出しています」) との間の矛盾です。ポインタ」)。
nullptr の利点は、整数型がないことです。オーバーロードされた関数 fun を nullptr で呼び出すと、void* オーバーロード (つまり、ポインター オーバーロード) が呼び出されます。
fun(nullptr); // calls fun(void*) overload
したがって、0 または NULL の代わりに nullptr を使用すると、オーバーロード解決の驚きを回避できます。
戻り値の型に auto を使用する場合のnullptr
overのもう 1 つの利点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 が渡されていることを意味し、それは予期されるstd::shared_ptr<int>
パラメーターと互換性がありませんf1
。への呼び出しで渡された 0 はlockAndCall
、null ポインターを表すことを意図していましたが、実際に渡されたのは int でした。この int を a として f1 に渡そうとするstd::shared_ptr<int>
と、型エラーになります。with 0の呼び出しはlockAndCall
失敗します。これは、テンプレート内で int がstd::shared_ptr<int>
.
関連する呼び出しの分析は、NULL
本質的に同じです。がNULL
に渡されるlockAndCall
と、パラメーター ptr に対して整数型が推定され、ptr
int または int に似た型が に渡されると型エラーが発生f2
しますstd::unique_ptr<int>
。
対照的に、関与する呼び出しnullptr
は問題ありません。がnullptr
に渡されるとlockAndCall
、 の型は でptr
あると推定されますstd::nullptr_t
。がptr
に渡されると、すべてのポインター型に暗黙的に変換されるため、f3
からstd::nullptr_t
への暗黙的な変換が行われます。int*
std::nullptr_t
null ポインターを参照する場合は、0 や ではなく nullptr を使用することをお勧めしますNULL
。
他の人がすでに言ったように、その主な利点はオーバーロードにあります。また、明示的なint
オーバーロードとポインターのオーバーロードはまれな場合がありますが、次のような標準ライブラリ関数を検討std::fill
してください (これは C++03 で何度も噛みつきました)。
MyClass *arr[4];
std::fill_n(arr, 4, NULL);
コンパイルしません: Cannot convert int to MyClass*
.
例を示した方法で持つことの直接的な利点はありません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)
これらのオーバーロードの問題よりも IMO の方が重要です。深くネストされたテンプレート構造では、型を見失うことは難しく、明示的な署名を与えることはかなりの努力です。したがって、使用するすべてのものについて、意図した目的に正確に焦点を合わせるほど、明示的な署名の必要性が減り、何か問題が発生したときにコンパイラがより洞察に満ちたエラー メッセージを生成できるようになります。