プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

C++/CLIで間違ったキャストをした時の挙動の実験

VisualStudio2012で確認。

c-style-cast, static_cast, const_cast, dynamic_cast, reinterpret_cast, safe_castの6種類のキャストを、以下の「間違った/意味のない」キャストに適応してみました。
キャスト先がStirngBuider^である理由は特にありません。

  • nullリテラル(nullptr) から StringBuilder^ へのキャスト
  • String^ から StringBuilder^ へのキャスト
  • String^ を Object^ を経由して StringBuilder^ へのキャスト
  • int* から StringBuilder^ へのキャスト
  • 文字列リテラル から StringBuilder^ へのキャスト

警告レベルは4にし、以下のソースのマクロの値を切り替えでビルドしました。実行可能であるなら「Debugビルドでデバッグ実行」「Debugビルドでデバッグなし実行」「Releaseビルドでデバッグ実行」「Releaseビルドでデバッグなし実行」の4パターン調べてみました。

using namespace System;
using namespace System::Text;

#define CAST_TYPE 0     // ここを変えてキャスト方法を変更
#define TARGET_TYPE 0   // ここを変えてキャストするものを変更

#if CAST_TYPE == 0
#define CAST(toType, target) ((toType)(target))
#elif CAST_TYPE == 1
#define CAST(toType, target) (static_cast<toType>(target))
#elif CAST_TYPE == 2
#define CAST(toType, target) (const_cast<toType>(target))
#elif CAST_TYPE == 3
#define CAST(toType, target) (dynamic_cast<toType>(target))
#elif CAST_TYPE == 4
#define CAST(toType, target) (reinterpret_cast<toType>(target))
#elif CAST_TYPE == 5
#define CAST(toType, target) (safe_cast<toType>(target))
#endif

#if TARGET_TYPE == 0
#define TARGET (nullptr)
#elif TARGET_TYPE == 1
#define TARGET (netStr)
#elif TARGET_TYPE == 2
#define TARGET ((Object^)netStr)
#elif TARGET_TYPE == 3
#define TARGET (&i)
#elif TARGET_TYPE == 4
#define TARGET ("abc")
#endif

int main(){
    String ^netStr = "abc";
    int i;
    try{
        StringBuilder ^builder = CAST(StringBuilder^, TARGET);
        if(builder == nullptr){
            Console::WriteLine("nullptr返却");
        }
        else{
            Console::WriteLine(builder->GetType());
            builder->Append("bad operation");
            Console::WriteLine(builder->ToString());
        }
    }
    catch(Exception ^ex){
        Console::WriteLine(ex->GetType() + " が " + ex->StackTrace + "で発生しました");
    }

    Console::ReadLine();    // ストッパー
    return 0;
}

結果は下のリスト・表のようになりました。
なお、「c-style-castを文字列リテラルに対して適応」以外では、DebugビルドとReleaseビルド、デバッグ実行とデバッグなし実行の差は見られませんでした。

c-style-cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

String^ から StringBuilder^ への変換

コンパイル結果: warning C4669: '型キャスト' : 変換は保証されません : 'System::Text::StringBuilder' は マネージ 型のオブジェクトです
実行結果: キャスト時に'System.InvalidCastException'が発生した

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: キャスト時に'System.InvalidCastException'が発生した

int* から StringBuilder^ への変換

コンパイル結果: error C2440: '型キャスト' : 'int *' から 'System::Text::StringBuilder ^' に変換できません。使用可能なユーザー定義された変換演算子がない、またはネイティブ型を マネージ 型に変換できません。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: warning C4669: '型キャスト' : 変換は保証されません : 'System::Text::StringBuilder' は マネージ 型のオブジェクトです
実行結果: デバッグなし実行時では、何も出力されること無くプログラムが動作を停止した。デバッグ実行時ではキャスト時に以下のダイアログが表示された。
「マネージ デバッグ アシスタント 'FatalExecutionEngineError' では '***.exe' に問題を検出しました。
追加情報: ランタイムの重大なエラーが発生しました。エラーのアドレスは 0x61f581be、スレッド 0x151c です。エラー コードは 0xc0000005 です。これは CLR のバグであるか、またはユーザー コードのアンセーフまたは確認不可能な部分にバグがある可能性があります。このバグの一般的な原因には、スタックが壊れる可能性のある COM-interop または PInvoke のユーザー マーシャリング エラーが含まれています。」

static_cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

String^ から StringBuilder^ への変換

コンパイル結果: error C2440: 'static_cast' : 'System::String ^' から 'System::Text::StringBuilder ^' に変換できません。使用可能なユーザー定義された変換演算子がない、または指示された型は関連がありません。変換には reinterpret_cast、C スタイル キャストまたは関数スタイルのキャストが必要です。

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: GetType()の戻り値は'System.String'となり、Append()呼び出し時に'System.NullReferenceException'が発生した

int* から StringBuilder^ への変換

コンパイル結果: error C2440: 'static_cast' : 'int *' から 'System::Text::StringBuilder ^' に変換できません。使用可能なユーザー定義された変換演算子がない、またはネイティブ型を マネージ 型に変換できません。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: error C2440: 'static_cast' : 'const char [4]' から 'System::Text::StringBuilder ^' に変換できません。理由: 'const char *' から 'System::Text::StringBuilder ^' へは変換できません。使用可能なユーザー定義された変換演算子がない、またはネイティブ型を マネージ 型に変換できません。

const_cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

String^ から StringBuilder^ への変換

コンパイル結果: error C2440: 'const_cast' : 'System::String ^' から 'System::Text::StringBuilder ^' に変換できません。指示された型は関連がありません。変換には reinterpret_cast、C スタイル キャストまたは関数スタイルのキャストが必要です。

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: error C2440: 'const_cast' : 'System::Object ^' から 'System::Text::StringBuilder ^' に変換できません。ポインターから基本クラスからポインターから派生クラスへの変換では明示的なキャストが必要です (const_cast 以外)。

int* から StringBuilder^ への変換

コンパイル結果: error C2440: 'const_cast' : 'int *' から 'System::Text::StringBuilder ^' に変換できません。ネイティブ型を マネージ 型に変換できません。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: error C2440: 'const_cast' : 'const char [4]' から 'System::Text::StringBuilder ^' に変換できません。ネイティブ型を マネージ 型に変換できません。

dynamic_cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: error C2681: 'nullptr' : dynamic_cast の式の型が無効です。

String^ から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

int* から StringBuilder^ への変換

コンパイル結果: error C2681: 'int *' : dynamic_cast の式の型が無効です。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: error C2681: 'const char [4]' : dynamic_cast の式の型が無効です。

reinterpret_cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

String^ から StringBuilder^ への変換

コンパイル結果: warning C4669: 'reinterpret_cast' : 変換は保証されません : 'System::Text::StringBuilder' は マネージ 型のオブジェクトです
実行結果: GetType()の戻り値は'System.String'となり、Append()呼び出し時に'System.NullReferenceException'が発生した

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: warning C4669: 'reinterpret_cast' : 変換は保証されません : 'System::Text::StringBuilder' は マネージ 型のオブジェクトです
実行結果: GetType()の戻り値は'System.String'となり、Append()呼び出し時に'System.NullReferenceException'が発生した

int* から StringBuilder^ への変換

コンパイル結果: error C2440: 'reinterpret_cast' : 'int *' から 'System::Text::StringBuilder ^' に変換できません。ネイティブ型を マネージ 型に変換できません。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: warning C4669: 'reinterpret_cast' : 変換は保証されません : 'System::Text::StringBuilder' は マネージ 型のオブジェクトです
実行結果: GetType()の戻り値は'System.String'となり、Append()呼び出し時に'System.NullReferenceException'が発生した

safe_cast

nullリテラル から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: nullptr返却

String^ から StringBuilder^ への変換

コンパイル結果: error C2682: 'safe_cast' を使用して 'System::String ^' から 'System::Text::StringBuilder ^' に変換することはできません。使用可能なユーザー定義された変換演算子がない、または指示された型は関連がありません。変換には reinterpret_cast、C スタイル キャストまたは関数スタイルのキャストが必要です。

((Object^)String^) から StringBuilder^ への変換

コンパイル結果: (警告もエラーもなし)
実行結果: キャスト時に'System.InvalidCastException'が発生した

int* から StringBuilder^ への変換

コンパイル結果: error C2682: 'safe_cast' を使用して 'int *' から 'System::Text::StringBuilder ^' に変換することはできません。使用可能なユーザー定義された変換演算子がない、またはネイティブ型を マネージ 型に変換できません。

文字列リテラル から StringBuilder^ への変換

コンパイル結果: 'safe_cast' を使用して 'const char [4]' から 'System::Text::StringBuilder ^' に変換することはできません。理由: 'const char *' から 'System::Text::StringBuilder ^' へは変換できません。使用可能なユーザー定義された変換演算子がない、またはネイティブ型を マネージ 型に変換できません。

まとめ

StringBuilder^にキャストしようとした際の挙動をまとめた表が下のものになります。

↓キャスト方法\キャスト対象→ nullリテラル String^ ((Object^)String^) int* 文字列リテラル
c-style-cast nullptr返却 (警告あり)キャスト時に例外発生 Append時に例外発生 コンパイルエラー (警告あり)キャスト時に「重大なエラー」発生
static_cast nullptr返却 コンパイルエラー Append時に例外発生 コンパイルエラー コンパイルエラー
const_cast nullptr返却 コンパイルエラー コンパイルエラー コンパイルエラー コンパイルエラー
reinterpret_cast nullptr返却 (警告あり)Append時に例外発生 (警告あり)Append時に例外発生 コンパイルエラー (警告あり)Append時に例外発生
dynamic_cast コンパイルエラー nullptr返却 nullptr返却 コンパイルエラー コンパイルエラー
safe_cast nullptr返却 コンパイルエラー キャスト時に例外発生 コンパイルエラー コンパイルエラー

結論

  • c-style-cast及びstatic_castは、誤ったダウンキャストを行なっていてもメソッド呼び出しを行うまでそれが判らず、かつダウンキャストを行う箇所に警告が出ないのが問題。
  • const_castは用途が用途なのでコンパイルエラーで当然。
  • reinterpret_castは警告を出してくれるだけ良心的。
  • dynamic_castは、成功するはずがないマネージ型の変換でも警告やエラーを出さないのは問題がありそう。
  • safe_castが、本当に意味のあるキャストだけが可能で、かつキャスト失敗時に例外という通知を出してくれるので、一番好ましいですね。

ところで、文字列リテラルをc-style-castした時は一体何が起きているのでしょうか。