演算:reinterpret_cast
Robert Schmidt
Microsoft Corporation
2000 年 6 月 1 日
前回は変換演算子 static_cast について解説しました。今回はその姉妹演算子 reinterpret_cast について解説するとともに、これらの演算子をどのようなときに使うべきか説明します。
標準規格では、static_cast に対して非常に汎用的な特性といくつかの複雑な例外が規定されていますが、reinterpret_cast に対してはその基本的な役割が次の 2 つに制限されています。
ポインタからの変換、およびポインタへの変換
複数の型による左辺値のオーバーレイ(型のパンニング。後述)
この方式のキャストはその名前が示すとおり、ある型のオペランドを、指定された別の型であるものとして再解釈します。この再解釈は変換コンストラクタや変換演算子への呼び出しを伴うことがありません。実際、reinterpret_cast ではオペランドのビット パターンがそのまま保たれ、変換はランタイムの結果にまったく影響しないコンパイル時の処理になります。
ポインタの変換
reinterpret_cast で許されているポインタ専用の変換の種類は、次のように非常に限られています。
ポインタから整数型への変換
整数型または列挙型からポインタへの変換
オブジェクト、関数、またはメンバを指すポインタどうしの変換
オブジェクトのサイズとアライメントが保証されているとすれば(詳細は以下で説明します)、次の一連の循環変換では x1 の元の値を復元できます。
T1 x1;
T2 x2;
x2 = reinterpret_cast<T2>(x1);
x1 = reinterpret_cast<T1>(x2);
このことが保証されている以外は、変換の影響は規定されていません。特に、変換によって変換後の値のビット表現が変わることもあれば、ビット パターンがそのまま保たれることもあります。後者の場合、reinterpret_cast は確かに再解釈を意味しますが、式のビット パターンが実際に変わることはありません。
このような変換においてはすべてポインタが関係する、といっても驚くことではありません。どのコンピュータでも、同じ性質のポインタの基本的表現は、ほとんどの場合まったく同じだからです。したがって、実行時に修正を必要としない静的な型変換を行う上で、ポインタは理想的な手段となります。
static_cast と同様に、reinterpret_cast は cv 修飾子を削除できません。そのような変換については、C++ 委員会のメンバはプログラマに対して const_cast を使うよう明確に要求しています。
ポインタからポインタ以外への変換、ポインタ以外からポインタへの変換
ポインタを整数型に変換するには、その整数型がポインタ値を保持できるほど十分に大きい必要があります。実際の変換のしかたは実装によって定義されますが、標準規格では、変換は「対象となるコンピュータのアドレス指定方式を理解している人を驚かすことがないようなものに」すべきだとしています。
逆に、整数型または列挙型の値をポインタに変換することもできます。また、整数がポインタを保持できるほど十分に大きければ、ポインタを整数に変換してから再び元に戻しても、元の値が保たれます。整数がポインタを保持できるほど十分に大きくなければ、変換の結果は実装によって異なります。
(Windows のすばらしい世界に住む人々にとっては、このような規則のおかげでポインタから DWORD への変換、またはその逆変換を問題なく行うことができます。)
特殊な値の 1 つに、null ポインタ定数があります。null ポインタ定数は値 0 の整数定数式として実装されており、ターゲットとなる型の null ポインタに必ず変換できます。null ポインタ以外の値 0 の整数式については、些細な違いですが、null ポインタに変換できる場合とできない場合があります。
#define NULL 0
const int i1 = 0;
int i2 = 0;
reinterpret_cast<void *>(NULL); // yields null pointer
reinterpret_cast<void *>(i1); // yields null pointer
reinterpret_cast<void *>(i2); // may or may not yield null pointer
ポインタどうしの変換
オブジェクト、関数、メンバ オブジェクト、メンバ関数など、同じ「種類」の実体を指すポインタどうしは、次に示す注意事項や制限事項のもとで相互に変換できます。
一般の場合と同様に、変換後の型の cv 修飾を元の型よりも緩やかにすることはできない。
オブジェクトまたはメンバ オブジェクトを指すポインタどうしを変換する際、変換後の型のアライメント条件を元のオブジェクト型よりも厳密にすることはできない。
元の型の null ポインタ値は、変換後の型の null ポインタ値に変換される。
これらの変換の例を示すプログラムの一部を次に示します。
class T1;
class T2;
//
// pointer to object
//
typedef T1 *O1;
typedef T2 *O2;
O1 o1;
reinterpret_cast<O2>(o1);
//
// pointer to function
//
typedef T1 (*F1)();
typedef T2 (*F2)();
F1 f1;
reinterpret_cast<F2>(f1);
//
// pointer to member object
//
typedef int T1:: *MO1;
typedef long T2:: *MO2;
MO1 mo1;
reinterpret_cast<MO2>(mo1);
//
// pointer to member function
//
typedef void (T1:: *MF1)();
typedef void (T2:: *MF2)();
MF1 mf1;
reinterpret_cast<MF2>(mf1); // OK
私が C99 および C++ の両方の標準規格を見た限りでは、異なる種類のポインタどうしを相互に変換することはできないようです。たとえば次のような変換を考えます。
reinterpret_cast<F1>(o1);
reinterpret_cast<O1>(f1);
私の解釈が正しいとすれば上の変換はコンパイルされないはずです。しかし、私が試した限りでは、EDG をストリクト モードで実行した場合を含め、どのトランスレータでも変換が許されました。幸いなことに、これらの非標準の変換は純粋な拡張仕様となっているようです。つまり、プログラムでこのような変換を使用しなければ影響を受けることはありません。
このような異種ポインタの変換を使用する場合は、非準拠のコード、つまり移植性のないコードを記述することになるので注意してください(ただし、使用するすべてのコンパイラでこの拡張仕様がサポートされているのであれば、実用上移植性のあるコードということになります)。
型のオーバーレイ
ここまで説明したポインタ変換のほかに、reinterpret_cast では、任意の型で左辺値をオーバーレイできます。たとえば、T1 という型の左辺値のアドレスを T2 という型のアドレスとして再解釈できる場合、T1 型の左辺値を T2 型への参照に結合できます。次の宣言を考えます。
T1 x1;
次の型変換
reinterpret_cast<T2 &>(x1);
は、次の変換
reinterpret_cast<T2 *>(&x1);
も許されている場合に許可されます(& および * は組み込みの演算子を表していると仮定)。
参照の変換はポインタ変換の逆参照に相当します。したがって、
reinterpret_cast<T2 &>(x1)
および
*reinterpret_cast<T2 *>(&x1)
は等価です。次の型変換
reinterpret_cast<T2 &>(x1);
は、&x1 を T2 * と再解釈してから T2 型の左辺値に逆参照したものとみなすことができます。ただし構文上は x1 が T2 型の左辺値に直接再解釈されるともみなせます。ふつう、参照は内部的にポインタとして実装されているため、このようなポインタと参照の相関関係も驚くことはありません。コンパイラで T2 * への変換が許されていれば、T2 * と T2 & は同じ表現を共有していると思われるため、コンパイラは T2 * をその対である T2 & として再解釈できます。
型変換演算子はオペランドが表現しているものを直接再解釈するわけではないので、この場合は reinterpret_cast という名前が誤解を招く恐れがあります(この型変換を前述のポインタ変換の例と比較してみてください。ポインタ変換ではオペランドが表現しているものが文字どおり再解釈されていました)。C++ 委員会のメンバは、reinterpret_cast を「オーバーロード」したときに実装上では共存が可能であることを利用したのです。つまり、基礎となっているポインタ再解釈のメカニズムにおいて、たまたま参照の再解釈も許されている、ということです。無論これらの概念は論理的には異なります。このような利点を採用すべきか、あるいはその代わりに 5 番目の変換演算子を定義したり型のパンニングを禁止したりするかということは、また別の問題ですが。
reinterpret_cast のほかの例とは違い、T2 & の変換では循環変換させた場合に T1 および T2 の値を保持しておく必要はなく、それらが相互に変換可能である必要すらありません。実際、変換可能でなければならないのはそれらの型を指す「ポインタ」だけです。さらに、T2 & は元の x1 というオブジェクトに直接結合しているため、一時オブジェクトなどはまったく作成されず、変換演算子やコンストラクタもまったく呼び出されません。
パンニング
プログラマはこの型変換技法を「型のパンニング(type punning)」と呼ぶことがあります。英語の pun には「語呂合わせ」という意味がありますが、型のパンニングによって、同じ実体に対して複数の意味を持たせることが可能になります。パンニングはポリモーフィズム(polymorphism)の概念に似ていますが、2 つの決定的な違いがあります。
ポリモーフィックな型は継承関係を持っていなければならないが、パンニングされた型はたとえ矛盾していても完全に無関係なものにできる。
ポリモーフィズムは実行時におけるオブジェクトの識別および変換に依存する。そのため実行時に多大な領域および速度のコストを招く恐れがある。パンニングでは実行時のコストは最小限か、またはまったく発生しない。唯一可能性のあるコストはポインタからポインタへの変換に伴うものであるが、実装形態によってはそのような変換でも実行時のリソースがまったく不要な場合もある。
共用体は、実際にはパンニングに近い概念といえます。どちらも次のような性質があります。
同じ格納域に対して任意の型のセットをオーバーレイする。
同じ格納域をある 1 つの型として設定し、それを別の型として参照できる。
言語固有の型変換規則をバイパスする(したがって予期できない動作を招く危険もある)
この類似性をさらに詳しく理解するために、共用体とパンニングのそれぞれの効果を考えてみます。
共用体
union
{
T1 x1;
T2 x2;
// ...
Tn xn;
} u;
u.x1; // treats the object as if it had type T1
u.x2; // treats the object as if it had type T2
// ...
u.xn; // treats the object as if it had type Tn
パンニング
T1 x1;
T2 &x2 = reinterpret_cast<T2 &>(x1);
// ...
Tn &xn = reinterpret_cast<Tn &>(x1);
x1; // treats the object as if it had type T1
x2; // treats the object as if it had type T2
// ...
xn; // treats the object as if it had type Tn
パンニングの結果
パンニングを使うと、異なるさまざまな型を必要な数だけ同じオブジェクトに結び付けることができます。この技法を最大限に活用するためには、オブジェクトをあたかも実際に異なる型であるかのように扱うことでどのような結果が得られるのかについて、理解する必要があります。次の例を考えてみましょう。
#include <iostream>
using namespace std;
int main()
{
unsigned long x1 = 0x12345678;
unsigned short &x2 = reinterpret_cast<unsigned short &>(x1);
cout <<hex <<x1 <<endl;
cout <<hex <<x2 <<endl;
x2 = 0xABCD;
cout <<hex <<x1 <<endl;
cout <<hex <<x2 <<endl;
}
Visual C++ を使ってこのプログラムを実行すると、次の結果が得られます。
12345678
1234
abcd5678
abcd
実際の x1 オブジェクトは x2 から参照しているはずのオブジェクトよりも大きいものです。そのため、x2 を介してアクセスしても x1 の一部しか「見る」ことができません。同様に、x2 を介して上書きを試みても、x1 の一部は上書きできますが、全部を上書きすることはできません。x2 からのアクセスは x1 の境界内部に限られているため、このプログラムは安全であるといえます。
ここで定義を入れ替えると、実際の x1 オブジェクトの型が小さすぎることになります。修正したプログラムは次のようになります。
#include <iostream>
using namespace std;
int main()
{
unsigned short x1 = 0x1234;
unsigned long &x2 = reinterpret_cast<unsigned long &>(x1);
cout <<hex <<x1 <<endl;
cout <<hex <<x2 <<endl;
x2 = 0xABCDEFAB;
cout <<hex <<x1 <<endl;
cout <<hex <<x2 <<endl;
}
少なくとも私のコンピュータでは次のような結果になりました。
1234
12340003
abcd
abcdefab
この時点では、x1 は x2 が境界とみなしている大きさよりも小さくなっています。その結果、私のコンピュータでは、x2 を介してアクセスすると x1 の境界を超えたメモリの内容を読み取り、x1 ではない余分な 0x0003 という数値が現れます。同様に、x2 を通じて代入を実行すると、x1 オブジェクトの範囲を超えて書き込みをするため、その結果プログラムの予期しない動作を招くことになります。私のコンピュータではプログラムは暴走しませんでしたが、あなたのコンピュータではどうなるかわかりません。
比較検討
const_cast および dynamic_cast についても正式に解説しなければなりませんが、これらは私が定義する(多少間違いも含まれている)次のような変換演算子使用規則の中に含めることにします。
cv 修飾子を減らす場合は、const_cast を使う。
ダウンキャストを行い、キャストの有効性を実行時に確定したい場合は、dynamic_cast を使う。
ビット パターンを再解釈し、ポインタが関係する場合は、reinterpret_cast を使う。
型のパンニングを利用する場合は、reinterpret_cast を使う。
ほかのすべての変換では、static_cast を使う。
(T) e という形式の C のキャスト、および T(e) という形式の関数キャストの使用は避ける。どちらの場合も変換演算子のほうが明確かつ正確である。
実行時の一般的な動作
reinterpret_cast が処理するのは、整数、列挙型、ポインタ、および参照だけに限られます。ユーザー定義の関数を呼び出すことはなく、ランタイム ライブラリを呼び出すこともおそらくありません。その上、ポインタ変換ではオペランドの範囲を拡張または制限するために若干の機械語命令を必要とする場合もあります(コンパイラがこのような変換をライブラリ ルーチンにパッケージ化することもできますが、実際そのような動作をするコンパイラを私は知りません)。
一方、static_cast は上記のすべての変換の他に、浮動小数点型やクラス型の変換も行います。static_cast の実行時の動作は変換される型に非常に強く依存します。たとえば次の変換を考えます。
int i;
static_cast<X>(i);
この変換は、X の型によっていくつかの異なる結果が得られます。
X がポインタ、整数、または列挙型ならば、reinterpret_cast と同じ結果になる。
X が浮動小数点型ならば、i が X に変換される。実装方法により、この変換ではインラインの浮動小数点命令、浮動小数点ライブラリ ルーチンへの呼び出し、または別の浮動小数点プロセッサへの処理の受け渡しが必要になる場合がある。
X がクラスならば、i は対応する X コンストラクタへの引数である。この X コンストラクタはあらかじめ宣言されていてアクセス可能でなければならない。
重複する動作
reinterpret_cast および static_cast には、次に示すいくつかの重複する動作があります。
void * とそれ以外のポインタ型との間の変換
基本ポインタと派生ポインタとの間の変換
派生された参照の基本オブジェクトへの結合
どちらの形式でも正常に動作するという場合でも、私はやはり reinterpet_cast の使用を推奨します。
reinterpret_cast という名前のほうが型の変換処理をより正確に表現している。
static_cast は C++ の道具の中で最も危険なツールの 1 つであり、ほかのすべての方法でうまくいかないときに限り使うべきである。
私としては、C++ の委員会が reintepret_cast と static_cast とを相互排他的な動作セットに分類してくれていたらと思います。これら 2 つの演算子の動作が重複しなければ、プログラマもそれぞれの演算子を使うべき場面を判断しやすかったでしょう。現状では、プログラマの多くがどちらを使うべきか正しく判断しているとは思えず、コンパイラに選択を任せているようです。
重複していない動作
逆に、予期したとおりに重複していない動作もいくつかあります。次の変換を考えます。
long l;
int i;
i = static_cast<int>(l);
Win32 をターゲットとしている Visual C++ では、long と int はまったく同じ表現になります。long から int への変換は、実際には既存の long 値の再解釈になります。実際、次の循環変換では元の l の値が復元されます。
i = static_cast<int>(l);
l = static_cast<long>(i);
したがって、reinterpret_cast で保証されている主要な動作と同じになります。
この結果から、次の
i = reinterpret_cast<int>(l);
という変換も正しく動作し、
i = reinterpret_cast<int>(l);
l = reinterpret_cast<int>(i);
では l の値が復元されると期待するかもしれません。しかし、このような reinterpret_cast は、たとえどちらも static_cast と同じ実装がなされているとしても、言語上の規則のために正しく動作しません。
規則は気まぐれなものではありません。long から int への変換は Win32 においては物理的な再解釈になりますが、すべてのプラットフォームで必ずしもそのような再解釈になるわけではありません。たとえば、Win16 では int と long のサイズが異なります。そのようなアーキテクチャにおいては、変換によって long の値が変わることがあり、したがって循環変換によって元の値が復元するという保証はありません。
それではまた
次のコラムの話題について決めておかなければならないのですが、すばらしいインスピレーションが湧かなくて悩みそうだ、とは思っていません。今皆さんが目にしているこの内容は 6 月 1 日に「公開」を予定しているものです。それから数日後にオレゴンに渡り、Scott Meyers の STL セミナーに出席する予定です。この出張で書くべき話題がたくさん得られることでしょう(Scott のコース教材を読んだだけでも数か月分のアイデアを書き留めることができました)。
Robert Schmidt は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています。