web-dev-qa-db-ja.com

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

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

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

それで、ポインターnullptrに値NULLまたは0を割り当てるよりも、ポインターを割り当てることの利点は何ですか?

156
Mark Garcia

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

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)が呼び出されます。

173
Nawaz

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として定義されている場合、上記の式はどちらも機能しません。

  • ケース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* ISであるため、0バージョンが望ましい場合があります_また、nullポインター。
  • ケース3:NULL0として定義されている場合、おそらくdoSomething(int)が意図されたときにdoSomething(char *)を呼び出し、おそらく結果実行時の論理エラー。 NULL0Lとして定義されている場合、呼び出しはあいまいであり、コンパイルエラーが発生します。

そのため、実装によっては、同じコードでさまざまな結果が得られることがありますが、これは明らかに望ましくありません。当然、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;
}

上記のプログラムでは、

  • ケース1:OK-プロパティ2
  • ケース2:Ok以外-プロパティ3
  • ケース3:OK-プロパティ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標準:4.10ポインター変換
C99標準:6.3.2.3ポインター

84
Alok Save

ここでの本当の動機は完全な転送です。

考慮してください:

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); }
23
Puppy

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を含む呼び出しの分析は基本的に同じです。 NULLlockAndCallに渡されると、パラメーターptrの整数型が推定され、ptr(intまたはintのような型)がf2に渡されると、型エラーが発生します。 std::unique_ptr<int>を取得する予定です。

対照的に、nullptrを含む呼び出しには問題はありません。 nullptrlockAndCallに渡されると、ptrの型はstd::nullptr_tであると推定されます。 ptrf3に渡されると、std::nullptr_tは暗黙的にすべてのポインター型に変換されるため、int*からstd::nullptr_tへの暗黙的な変換が行われます。

推奨されます。nullポインターを参照する場合は、0またはNULLではなくnullptrを使用してください。

5
Ajay yadav

他の人がすでに言っているように、その主な利点は過負荷にあります。明示的なint対ポインターのオーバーロードはまれですが、std::fill(C++ 03で2回以上噛まれた)のような標準ライブラリ関数を検討してください。

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

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

4
Angew

例を示した方法で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)
4
iammilind

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

2
leftaroundabout