次のコード概念的には、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
ポインター定数として知られ、Itは型安全性を向上させますおよびは、既存の実装依存の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
または(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
または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
}
上記のスニペットの分析:
doSomething(char *)
を呼び出します。doSomething(int)
を呼び出しますが、char*
ISであるため、0
バージョンが望ましい場合があります_また、nullポインター。NULL
が0
として定義されている場合、おそらくdoSomething(int)
が意図されたときにdoSomething(char *)
を呼び出し、おそらく結果実行時の論理エラー。 NULL
が0L
として定義されている場合、呼び出しはあいまいであり、コンパイルエラーが発生します。そのため、実装によっては、同じコードでさまざまな結果が得られることがありますが、これは明らかに望ましくありません。当然、C++標準委員会はこれを修正することを望んでおり、それがnullptrの主要な動機です。
nullptr
とは何ですか?NULL
の問題をどのように回避しますか?C++ 11では、nullポインター定数として機能する新しいキーワード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;
}
上記のプログラムでは、
char *
バージョンの呼び出し、プロパティ2および3したがって、nullptrを導入すると、古き良きNULLの問題がすべて回避されます。
nullptr
はどのように、どこで使用すべきですか?C++ 11の経験則は、過去にNULLを使用していた場合は常にnullptr
の使用を開始するだけです。
標準参照:
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
を導入することは絶対に必要でした。ここで、typeは特別なものであり、そのタイプは実際に伝播できます。実際、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の型です。タイプstd::nullptr_t
のprvalue/rvalueです。 nullptrから任意のポインター型のnullポインター値への暗黙的な変換が存在します。
リテラル0はintであり、ポインターではありません。 C++が、ポインタのみを使用できるコンテキストで0を見ていることに気付いた場合、C++は、しぶしぶ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を使用してfunを呼び出しています-nullポインター」)と実際の意味(「nullではなく整数でfunを呼び出しています」)の矛盾です。ポインター」)。
nullptrの利点は、整数型を持たないことです。 nullptrでオーバーロードされた関数funを呼び出すと、void *オーバーロード(つまり、ポインターのオーバーロード)が呼び出されます。これは、nullptrを不可欠なものと見なすことができないためです。
fun(nullptr); // calls fun(void*) overload
したがって、0またはNULLの代わりにnullptrを使用すると、オーバーロードの解決の驚きを回避できます。
戻りタイプにautoを使用する場合のNULL(0)
に対するnullptr
のもう1つの利点
たとえば、コードベースでこれに遭遇するとします:
auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}
FindRecordが何を返すかを知らない(または簡単に見つけられない)場合、結果がポインター型か整数型かが明確でない場合があります。結局、0(結果のテスト対象)はどちらの方法でも可能です。一方、次の場合は、
auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}
あいまいさはありません。結果はポインタ型でなければなりません。
利点
#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
のテンプレートを作成できる場合、このようなコードを作成するのは残念です。したがって、テンプレートで一般化できます。冗長コード用に複数の定義lockAndCallF1, lockAndCallF2 & lockAndCallF3
の代わりにテンプレート関数lockAndCall
を作成しました。
コードは次のようにリファクタリングされます。
#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
パラメーターと互換性がないことを意味します。 lockAndCall
への呼び出しで渡された0は、nullポインターを表すことを目的としていましたが、実際に渡されたのはintでした。このintをstd::shared_ptr<int>
としてf1に渡そうとすると、型エラーになります。テンプレート内では、std::shared_ptr<int>
を必要とする関数にintが渡されるため、0を使用したlockAndCall
の呼び出しは失敗します。
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またはNULL
ではなくnullptrを使用してください。
他の人がすでに言っているように、その主な利点は過負荷にあります。明示的なint
対ポインターのオーバーロードはまれですが、std::fill
(C++ 03で2回以上噛まれた)のような標準ライブラリ関数を検討してください。
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*);
NULLを渡すことでfoo(int*)
を呼び出したい場合、その方法は次のとおりです。
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の方が重要です。深くネストされたテンプレート構造では、型を追跡することは難しくなく、明示的な署名を与えることは非常に努力です。したがって、使用するすべてのものについて、意図した目的により正確に焦点を当てればするほど、明示的な署名の必要性が減り、何か問題が発生したときにコンパイラがより洞察に富んだエラーメッセージを生成できるようになります。