この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
38. 今後は NULL の代わりに nullptr を使用する
新しい C++ 標準では多くの有用な変更がありました。そのいくつかは、大きな利点をもたらすため、今すぐ取り入れるべきであると私は考えています。
その 1 つが、NULL マクロの代替である nullptr (英語) キーワードです。
C++ では、NULL は 0 と定義されています。
これは、単なる糖衣構文のように見えるかもしれません。nullptr でも、NULL でも違いがないと思われるかもしれません。しかし、違うのです! nullptr を使用することで、さまざまなエラーを回避できます。このことを次に示します。
2 つの多重定義関数があるとします。
void Foo(int x, int y, const char *name); void Foo(int x, int y, int ResourceID);
次の呼び出しについて考えてみます。
Foo(1, 2, NULL);
プログラマーは、1 つ目の関数を呼び出しているつもりですが、実際には、NULL は 0 であり、0 は int 型であるため、1 つ目の関数ではなく 2 つ目の関数が呼び出されます。
nullptr を使用した場合、このような間違いは起こらず、1 つ目の関数が呼び出されます。別のよくある NULL の使用例として、次のようなコードがあります。
if (unknownError) throw NULL;
私は、ポインターを渡す例外の生成を疑わしく思っていますが、一部のプログラマーは行っています。上記の例では、プログラマーはこの方法でコードを記述する必要があったのかもしれません。そうすることが良いか、悪いかについての議論はトピックから外れるため、ここでは取り上げません。
ここで重要なことは、プログラマーが不明なエラーの場合に例外を生成し、外部に null ポインターを「渡して」いることです。
実際には、これはポインターではなく int です。その結果、プログラマーが想定しない方法で例外が処理されます。
“throw nullptr;” コードはこの問題を解決してくれますが、私は完全に納得しているわけではありません。
一部のケースでは、nullptr を使用すると、不正なコードはコンパイルしません。
HRESULT 型を返す WinApi 関数があるとします。HRESULT (英語) 型は、ポインターとは関係がありません。しかし、次のような無意味なコードを記述する可能性は十分にあります。
if (WinApiFoo(a, b, c) != NULL)
NULL は int 型の 0 で、HRESULT は long 型であるため、このコードはコンパイルします。int 型と long 型の値の比較は可能です。nullptr を使用すると、次のコードはコンパイルしません。
if (WinApiFoo(a, b, c) != nullptr)
コンパイルエラーになることで、プログラマーがエラーに気付き、コードを修正できます。
問題の趣旨はお分かりいただけたかと思います。このような例が多数あります。しかし、その多くは合成された例で、説得力に欠けます。実例はないのでしょうか? あります。ここでは、そのうちのいくつかを紹介します。これらのコードは、あまり洗練されておらず、短くもありません。
MTASA プロジェクトから抜粋した以下のコードについて考えてみます。
RtlFillMemory() (英語) があります。これは、実関数またはマクロです。ここでは、どちらであるかは重要ではありません。memset() 関数に似ていますが、第 2 引数と第 3 引数が入れ替わっています。このマクロは、次のように宣言できます。
#define RtlFillMemory(Destination,Length,Fill) \ memset((Destination),(Fill),(Length))
FillMemory() というものもあります。これは、RtlFillMemory() です。
#define FillMemory RtlFillMemory
前述のとおり、長く複雑ですが、エラーを含む実際のコード例です。
以下が、FillMemory マクロを使用するコードです。
LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs ) { .... PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ; FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ; .... }
このコードには、複数のバグがあります。少なくとも、第 2 引数と第 3 引数が混同されていることが分かります。そのため、アナライザーは 2 つの V575 (英語) 警告を出力しています。
- V575 The ‘memset’ function processes value ‘512’. Inspect the second argument. crashhandler.cpp 499 (V575 ‘memset’ 関数が値 ‘512’ を処理しています。第 2 引数を確認してください。crashhandler.cpp 499)
- V575 The ‘memset’ function processes ‘0’ elements. Inspect the third argument. crashhandler.cpp 499 (V575 ‘memset’ 関数が ‘0’ 要素を処理しています。第 3 引数を確認してください。crashhandler.cpp 499)
NULL は 0 であるため、コードはコンパイルされます。その結果、0 配列要素に値が格納されます。しかし、実際に問題となるのは、このエラーではありません。一般に、NULL はここでは不適切です。memset() 関数はバイトを操作するため、メモリーに NULL 値を格納することは無意味です。正しいコードは、次のようになります。
FillMemory(pSym, SYM_BUFF_SIZE, 0);
あるいは、次のようになります。
ZeroMemory(pSym, SYM_BUFF_SIZE);
しかし、これは重要ではありません。ここで重要なことは、この無意味なコードが問題なくコンパイルされるということです。もしプログラマーに NULL の代わりに nullptr を使用する習慣があれば、次のようなコードを記述したでしょう。
FillMemory(pSym, nullptr, SYM_BUFF_SIZE);
そして、コンパイラーがエラーメッセージを出力し、プログラマーは問題に気付き、コーディング方法にもっと注意を払うでしょう。
注: この場合、NULL を非難すべきでないことは理解しています。しかし、NULL のせいで不正なコードが警告なしにコンパイルされています。
推奨事項
今すぐ nullptr を使用し、コーディング基準を変更することを推奨します。
nullptr は愚かなエラーの回避に役立つため、開発プロセスをわずかにスピードアップします。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。