C++11 では NULL ではなく nullptr を使う - C++ プログラミング

PROGRAM


C++11 では NULL ではなく nullptr を使う

C++ では NULL は 0 で定義されているため、オーバーロードされた関数を呼び出すようなときに予期しない動作をすることがありました。そんな問題を補うために C++11 では nullptr というキーワードが用意されています。

ここでは、従来の NULL と C++11 で規定された nullptr とを比較しながら、それらの使い方について見て行きたいと思います。

 

NULL や nullptr の効果

NULL や nullptr は、ポインタに格納されている値が「何もない」とか「空である」といった意味を持たせるためのキーワードです。

このようなとき、最終的にはポインタに 0 が代入されるのですけど、例えば次のように単に 0 を代入しただけでは、プログラムには「0 を代入している」以上の意味が込められません。

int *p = 0;

そこで C++ では、NULL や nullptr という、これに変わるキーワードが用意されています。

C++11 の場合

int *p = nullptr;

従前の C++ の場合

int *p = NULL;

厳密には、従前の C++ で使っていた NULL はキーワードではなくて #define で登録されているマクロです。

C 言語では NULL は "void(0)" で定義されていることが多いのですが、C++ の場合は "0" で定義されていることが多いようです。Xcode 4.5.2 では "__null" というキーワードで定義されているようでしたが、動きは "0" と基本的には変わらないようでした。

 

void* から別の型へは暗黙キャストされない

 

C 言語から C++ に発展する過程で NULL に定義する値が "void(0)" から "0" に変わった背景には、C++ では型の扱いがより厳密になったことが背景にあるようです。

C++ では void* 型から別のポインター型への暗黙キャストが行えないため、明示的にキャストしないと原則として警告になります。ちなみに C 言語では void* は別のポインター型へ暗黙的にキャストされます。

 

これがよく判るのが、メモリの確保に使う malloc を使ったメモリ確保です。

C の場合

int *p = malloc(100);

C++ の場合

int *p = (int*)malloc(100);

暗黙キャストされるかどうかの違いにより、C では必要なかった戻り値 void* のキャストが C++ では必要になります。

C++ の場合はメモリ確保には大抵 new を使うことになるのであまり影響ないかもしれませんが、他の関数などで void* を返すものの場合は同じように型のキャストが必要になります。

 

このような仕様上の理由から、C++ の NULL が C と同じ "(void*)0" で定義されていたとすると、それを使っている多くのソースコードを修正して、明示的にキャストするコードを追加しなくてはいけなくなります。

ただ、C 言語でも C++ でも、ポインタへの 0 代入は空のポインタを代入するという意味になるため、C++ では NULL を "0" で定義することで、既存のコードを調整することなく C++ に対応できます。

 

ちなみに Visual Studio 2012 では void* を他のポインター型に代入してもコンパイルが正常に通るようですが、Xcode 4.5.2 では警告になるので、原則として明示キャストが必要なことは意識しておいた方が良さそうです。

 

また、この NULL が "0" であるか "(void*)0" であるかというのは、あくまでも便宜上のもののようで、コンパイラによってどちらを採用しているか、あるいはまた別の何かが定義されているか、事情が違ってくる場合もあるようです。

少し異質な言語ですけど、C++ と互換性のある Xcode 4.5.2 の Objective-C++ では NULL は "(void*)0" で定義されるようで、NULL を void* 以外のポインタに代入しようとしたときには、代入先の型にキャストする必要がありました。

 

NULL が "0" であることの弊害と対処

C++ では void* が暗黙キャストされない都合から、NULL の定義が C 言語のときの "(void*)0" から単なる "0" になりました。

これによって、C 言語のときの NULL に含まれていた void* 型という型情報、つまりポインター型の 0 であるという型情報が抜け落ちてしまったことになります。

これにより、関数の引数に直接 NULL を渡すようなときに、整数の 0 と勘違いされてしまう場合があります。

 

オーバーロードされた関数を実行し間違える場合がある

指定された型が違うというコンパイルエラーになるだけなら話は簡単なのですが、たとえば C++ で登場したオーバーロード(多重定義)という仕組みと合わせて使ったときに、予期しない動作結果を招いてしまったりします。

オーバーロードというのは、同じ関数名でも異なる引数を取って、引数に応じた処理を行える仕組みです。

void setDescription(char* buffer, int value);

void setDescription(char* buffer, void* value);

たとえばこのような setDescription という関数の定義があったとき、引数に int 型が渡されたときには上の方が、ポインタ型が渡されたときには下の方が、コンパイラによって自動的に切り替えられて呼び出されます。

 

このとき、C++ では NULL には型を示す void* が含まれていないため、setDescription の第二引数に直接 NULL を渡してしまうと int 型が渡されたときの方が呼び出されてしまいます。

NULL という言葉には「空のポインターを示す」という期待が込められているので、それとは違う方の関数が選ばれることが、プログラムのバグに繋がる危険性があります。

 

この問題を避けるために、C++11 と従来の C++ ではそれぞれ、次のような対応を取ります。

C++11 の場合

setDescription(buffer, nullptr);

従前の C++ の場合

setDescription(buffer, (void*)NULL);

C++11 に対応しているコンパイラの場合は、空のポインタであるという意味の nullptr キーワードを使うことで、確実に第二引数にポインタを取る方が呼び出されます。

C++11 に対応していない場合には、NULL の型がクラスのポインタであることを明示的にキャストすることで、第二引数にポインタを取る方の関数を呼び出すことができます。

 

ちなみに NULL をポインタ型の変数に代入した後であれば、その変数を第二引数に指定すれば、ポインタを取る方の関数が呼ばれます。

void* value = (void*)NULL;

 

setDescription(buffer, value);

変数宣言のときに変数名に型情報がしっかり定義されているので、その変数が NULL だろうと別の値だろうと、コンパイラがその変数の型を認識し間違えることはありません。

 

NULL と nullptr の特徴

NULL も nullptr も最終的には、空のポインタを示す 0 を意味するキーワードです。

これらの違いは、コンパイラがそれを空のポインタを示すものであるかを知っているかどうかです。

 

NULL は単なるマクロのため、コンパイラにとっては「マクロに定義されている値」以上のことは知りません。それに対して nullptr は、コンパイラによって用意された「空のポインタ」であることを示すキーワードです。

そのため、NULL と比べてより厳密な制御がコンパイラによって行えるようになっています。

 

まず、この nullptr を代入できるのは、ポインター型として定義されている変数だけです。

void* p1 = nullptr;

char* p2 = nullptr;

int のような整数値として定義されている変数への代入はできません。

代入後は、従来の NULL を代入した時とまったく同じに使えます。

 

これらを void* 型にキャストしてどのアドレスを指しているかを調べてみると、どちらとも 0x0 を示していました。

free 関数や new 演算子に、NULL や nullptr の代入されたポインタを渡しても、エラーにならずに無視されます。

 

条件判定での特徴

iNULL や nullptr を条件文内で直接指定して比較するときに、コンパイル上でそれぞれの違いが現れてきます。

 

直接 NULL を条件判定に使うと、NULL の定義によっては int 型との判定ができる場合があります。

int a;

 

if (a == NULL)

{

}

これは、NULL があくまでも 0 を意味するためで、これができた場合は a に格納されている値が 0 であるかを判定することになります。

NULL を 0 として扱っているので、a が 0 かを判定できることが間違っているわけではありませんが、NULL に込められた期待とは動きがちょっと違います。

このような判定ができない場合でも、コンパイラは警告を出すだけになることが多いと思います。

 

直接 nullptr を指定した場合には、比較対象はポインター型の変数に限られます。

char* a;

 

if (a == nullptr)

{

}

こちらの nullptr を使う場合は、逆に int 型などのポインタ以外の型と比較しようとした場合にはコンパイルエラーとなるので、確実にポインタとの比較のためだけに使うことになります。

意図的に (int)nullptr みたいにキャストしてしまえば int 型との判定も出来てしまいますけど、わざわざそんなことをする価値はないでしょう。

 

なお、ポインタ型の変数に代入し終わった後であれば、NULL だろうと nullptr だろうと、同じ空のポインタとして扱われます。

char* a = nullptr;

char* b = NULL;

 

if (a == b)

{

}

そのため、それぞれを代入した同士を比較しても、両方ともヌルポインタなので true となります。

 

ちなみにまったく意味はありませんが、"if (NULL == nullptr)" というように、それぞれを直接指定して判定をしようとするとコンパイルエラーになる場合があります。

このようなことが起こるのは NULL が "0" で定義されているときで、nullptr にとっては NULL は 0 であってポインタではないためです。

NULL が "(void*)0" で定義されている場合には、それはポインター型を意味しているので nullptr と直接比較できて、true として判定されます。


[ もどる ]