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

この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。


6. ポインターを明示的に整数型へキャストしているすべてのコードを確認する

IPP Sample プロジェクトから抜粋した以下のコードについて考えてみます。このエラーは、次の PVS-Studio 診断によって検出されます。

V205 Explicit conversion of pointer type to 32-bit integer type: (unsigned long)(img) (ポインター型から整数型への明示的な変換: (unsigned long)(img))

void write_output_image(...., const Ipp32f *img, 
                        ...., const Ipp32s iStep) {
  ...
  img = (Ipp32f*)((unsigned long)(img) + iStep);
  ...
}

注: いくつかの理由から、このコードは良い例ではないという意見があります。ここでは、プログラマーがこのような奇妙な方法でデータバッファーを移動する必要性については考慮しません。ポインターが明示的に unsigned long 型にキャストされていることだけが重要です。私がこの例を選んだのは、単に簡潔だからです。

説明

プログラマーは、あるバイト数分だけポインターをシフトしたいと考えています。Win32 モードでは、ポインターサイズが long 型のサイズと同じため、このコードは正しく動作します。しかし、このプログラムの 64 ビット・バージョンをコンパイルすると、ポインターは 64 ビットになり、long へのキャストで上位ビットが失われます。

注: Linux* は、異なるデータモデル (英語) を使用します。64 ビットの Linux* プログラムでは、’long’ 型も 64 ビットですが、’long’ でポインターを格納するのは良いアイデアとは言えません。1 つ目の理由として、そのようなコードは、Windows* アプリケーションで多く見受けられ、その場合正しく動作しません。2 つ目に、ポインターを格納するための特殊な型、ポインター型整数 (例: intptr_t) があり、それらを使用したほうがプログラムは理解しやすくなります。

上記の例では、64 ビット・プログラムで発生する古典的なエラーが見られます。前もって言っておきますが、64 ビット・ソフトウェア・プログラミングでは、多くのその他のエラー (英語) がプログラマーを待ち構えています。その中でも、ポインターの 32 ビット整数変数への変換は、最も広範囲で見かける厄介な問題です。

このエラーは、次のように図解することができます。

図 1.  A) 32 ビット・プログラムB) 下位アドレスにあるオブジェクトを参照する 64 ビット・ポインターC) 破損した 64 ビット・ポインター

図 1. A) 32 ビット・プログラム B) 下位アドレスにあるオブジェクトを参照する 64 ビット・ポインター C) 破損した 64 ビット・ポインター

この図からも分かるように、このエラーは気付くのが非常に困難な場合があります。プログラムが「ほぼ正しく動作する」ためです。ポインターの最上位ビットの損失を引き起こすエラーは、プログラムを数時間使用し続けないと発生しないかもしれません。メモリーは下位メモリーアドレスに割り当てられるため、すべてのオブジェクトと配列はメモリーの最初の 4GB に格納されます。最初は、すべてが問題なく動作します。

プログラムを実行し続けると、メモリーが断片化し、プログラムがメモリーをそれほど使用しない場合であっても、新しいオブジェクトは最初の 4GB 以外に作成されることがあります。そこからが問題の始まりです。このような問題を意図的に再現することは非常に困難です。

正しいコード

ポインターを格納するための size_t、INT_PTR、DWORD_PTR、intrptr_t などのポインター型整数を使用します。

img = (Ipp32f*)((uintptr_t)(img) + iStep);

明示的なキャストは省略できます。書式が標準とは異なるという言及はないため、通常どおり __declspec(align( # )) などを使用できます。Ipp32f で割り切れるバイト数分だけポインターがシフトされます。そうでない場合、未定義の動作になります (「EXP36-C」 (英語) を参照)。

つまり、次のように記述できます。

img += iStep / sizeof(*img);

推奨事項

intlong のことは忘れて、ポインターを格納するための特殊な型を使用します。最も一般的な型は intptr_tuintptr_t です。Visual C++* では、次の型を利用できます: INT_PTRUINT_PTRLONG_PTRULONG_PTRDWORD_PTR。それぞれの名前が示すとおり、安全にポインターを格納できます。

size_tptrdiff_t 型を使用することもできますが、これらは元々サイズとインデックスを格納するためのものなので、ポインターの格納には推奨されません。

クラスのメンバー関数へのポインターを uintptr_t に格納することはできません。 メンバー関数は、標準関数とはやや異なります。ポインター自身を除いて、メンバー関数にはオブジェクト・クラスへのポインターである this という非表示の値があります。ただし、これは問題にはなりません。32 ビット・プログラムでは、このポインターを unsigned int に代入することはできません。このポインターは、常に特別な方法で処理されるため、64 ビット・プログラムでも多くの問題はありません。少なくとも、私はこれまでにそのようなエラーを見たことがありません。

プログラムの 64 ビット・バージョンをコンパイルする場合、最初にポインターが 32 ビット整数型にキャストされている個所をすべて確認し修正する必要があります。プログラムにはその他の問題のコード領域があるかもしれませんが、まずはポインターから開始すべきです。

64 ビット・アプリケーションの作成を計画中または開発中の場合、「64 ビット C/C++ アプリケーション開発に関するレッスン」 (英語) をお読みになることを推奨します。

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

関連記事