C++ キャストの意味と危険性
C++ の3つのキャストは Cスタイルのキャストの持つ3つの側面をそれぞれ独立させたものです
static_cast
static_cast はセマンティクスを維持したまま型を変えたいときに使います
static_cast はビットパターンを変更する場合があります
static_cast は変換のためにコードを生成する場合があります
float と int を相互に変換したり、基底クラスのポインタと派生クラスのポインタとを相互に変換したするような、ビットが変わってもセマンティクスが維持されていればいいという場合には static_cast を使います
1 と 1.0f はビットパターンは違いますが 1 という意味においては同一ですね
C++ でのキャストは基本的にはこの static_cast を使います
reinterpret_cast
reinterpret_cast は値を維持したまま型を変更する場合に使います
ビットパターンは変化しません
reinterpret_cast が変換のためのコードを生成することもありません
整数に入れておいたアドレスをポインタに戻したり、逆にポインタに入っているアドレスを整数に入れたりというような、ビットは変更せず単純にシンタックス上の型解釈の変更をするだけの場合には reinterpret_cast を使います
256.0f のビットパターンは 0x43800000 です。というときに使います
といいたいところですが、これは strict-aliasing-rules に反します
規格上の定義は次の通り:
- 指定した型が参照なら結果は lvalue になります。そうでないなら rvalue になります
このとき lvalue-to-rvalue、配列→ポインタ、関数→ポインタ、または標準の変換が適用されます - 型指定で型を定義することはできません
- constを剥がす目的には使えません
- ポインタに入っているアドレスを十分に大きいサイズの整数型に入れることができます
- 数値型や enum型に入っている値をアドレスとしてポインタに入れることができます
- 関数ポインタを別の型の関数ポインタに変更することができます
- ポインタを別の型のポインタに変更することができます
ただし型のメモリアライメントが一致している必要があります - 関数ポインタとデータポインタの交換は基本的には処理系定義です
- nullポインタは常に別の型の nullポインタに変換できます
- メンバ変数のポインタは別の型のメンバ変数のポインタに変換できます
メンバ関数のポインタについても同様です - 参照へのキャストをしても一時オブジェクトが作られたり余計な演算子が呼ばれたりはしません
reinterpret_cast は strict-aliasing-rules と非常に相性が悪いです
strict-aliasing-rules が有効になっているところでは使用しない方がいいでしょう
詳しくは「C++ エイリアスと最適化の危険な関係」に譲ります
const_cast
const_cast は型は変更せず const や volatile の有無だけを変更したい場合に使用します
ビットパターンは変化しません
変換のためのコードが生成されたりもしません
Cスタイルのキャスト
Cスタイルのキャストは static_cast できなければ reinterpret_cast するというものです
const や volatile の有無は気にしません
まず static_cast を試みるのでビットパターンが変わる場合があります
static_cast できなくても reinterpret_cast をするので問題をコンパイル時に検出できない場合があります
規格上の評価の順番は次の通り:
5.4 Explicit type conversion (cast notation) 4 The conversions performed by - a const_cast (5.2.11), - a static_cast (5.2.9), - a static_cast followed by a const_cast, - a reinterpret_cast (5.2.10), or - a reinterpret_cast followed by a const_cast, can be performed using the cast notation of explicit type conversion.
蛇足:
ここで言う「試みる」とか「評価の順序」というのは実行時の処理のことではなくコンパイル時の処理のことです
Cスタイルのキャストを使ったときに実行時にこの順番に条件分岐をするようなコードが生成されるというわけではありません
整数と浮動小数点数
浮動小数点数のビットパターンが欲しい時にキャストを使ってはいけません
最適化でおかしなコードが出力される場合があります
×ダメ
uint32_t u = 0; float32_t f = *reinterpret_cast<volatile const float32_t*>(&u);
union を使います
template<class result_type, class argument_type> inline result_type float_cast( const argument_type& argument ){ #include "pragma_pushpack1.h" volatile union float_cast_t { argument_type argument; result_type result; } cast; #include "pragma_poppack.h" cast.argument = argument; return cast.result; }
○よい
uint32_t u = 0; float32_t f = float_cast<float32_t>(u);
基底クラスと派生クラス
基底クラスと派生クラスのキャストには必ず static_cast を使います
class BBB1 {}; class BBB2 {}; class DDD1 : public BBB1, public BBB2 {}; // 1, 2 の順番 class DDD2 : public BBB2, public BBB1 {}; // 2, 1 の順番 int main(){ DDD1 ddd1; DDD2 ddd2; printf( " &ddd1 = %p\n", &ddd1 ); printf( " static_cast<BBB1*>(&ddd1) = %p\n", static_cast<BBB1*>(&ddd1)); printf( " static_cast<BBB2*>(&ddd1) = %p\n", static_cast<BBB2*>(&ddd1)); printf( "reinterpret_cast<BBB1*>(&ddd1) = %p\n", reinterpret_cast<BBB1*>(&ddd1)); printf( "reinterpret_cast<BBB2*>(&ddd1) = %p\n", reinterpret_cast<BBB2*>(&ddd1)); printf( " &ddd2 = %p\n", &ddd2 ); printf( " static_cast<BBB1*>(&ddd2) = %p\n", static_cast<BBB1*>(&ddd2)); printf( " static_cast<BBB2*>(&ddd2) = %p\n", static_cast<BBB2*>(&ddd2)); printf( "reinterpret_cast<BBB1*>(&ddd2) = %p\n", reinterpret_cast<BBB1*>(&ddd2)); printf( "reinterpret_cast<BBB2*>(&ddd2) = %p\n", reinterpret_cast<BBB2*>(&ddd2)); return 0; }
出力例:
&ddd1 = 0036FEA3 static_cast<BBB1*>(&ddd1) = 0036FEA3 ← BBB1 が基底クラスと同じ場所 static_cast<BBB2*>(&ddd1) = 0036FEA4 reinterpret_cast<BBB1*>(&ddd1) = 0036FEA3 reinterpret_cast<BBB2*>(&ddd1) = 0036FEA3 ← BBB2 も基底クラスと同じ場所(間違い!) &ddd2 = 0036FE97 static_cast<BBB1*>(&ddd2) = 0036FE98 static_cast<BBB2*>(&ddd2) = 0036FE97 ← BBB2 が基底クラスと同じ場所 reinterpret_cast<BBB1*>(&ddd2) = 0036FE97 ← BBB1 も基底クラスと同じ場所(間違い!) reinterpret_cast<BBB2*>(&ddd2) = 0036FE97
正しいコードでは static_cast も reinterpret_cast も同じコードを出力します
そうでないときに static_cast はコンパイル時に警告を出してくれるので安心ですが、reinterpret_cast は実行時に死ぬまでわからないので危険です
単純な派生関係では reinterpret_cast でも問題は起きませんが多重継承があると派生クラス内の基底クラスの位置(アドレス)が変わるので reinterpret_cast だと正しいインスタンスを取得できなくなります
そういうことがあるので基底クラス派生クラスのキャストには必ず static_cast を使います
reinterpret_cast は整数とポインタの変換とか signed/unsigned のポインタの変換などに使うくらいで、他は static_cast と const_cast で済むはずです
Cスタイルのキャストは static_cast できなければ reinterpret_cast するというものなので、基底クラス派生クラスのキャストに使うには reinterpret_cast より危険性は少ないけど static_cast よりは危険という感じです
dynamic_cast
dynamic_cast は仮想継承や RTTI(実行時型識別)といった機能と共に使うものです
この機能を使っていいか悪いかはプロジェクトによって違うのでプロジェクトの方針に従ってください
COM の QueryInterface なんかは dynamic_cast と同じ意味の機能です
dynamic_cast は実行時の機能なので他の3つとはちょっと性質が異なります