プログラミング、リファクタリング、そしてすべてにおける究極の疑問: No. 38

その他インテル® DPC++/C++ コンパイラー特集

この記事は、インテル® デベロッパー・ゾーンに公開されている「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)

NULLint 型の 0 で、HRESULTlong 型であるため、このコードはコンパイルします。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 は愚かなエラーの回避に役立つため、開発プロセスをわずかにスピードアップします。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

タイトルとURLをコピーしました