VC.NET ~ 新キャスト表記safe_cast

前置き

標準C++で規定されている4つのキャスト表記static_cast、dynamic_cast、const_cast、及びreinterpret_castに加えて、C++/CLIにはsafe_castというのがあります。それぞれに用途や挙動が規定されていますが、状況によっては、どれを使うべきか迷うこともありそうです。

そこで、具体例を使って、適用可否や挙動(nullが返るのか例外が発生するのか)を調べてみます。

本文

基礎知識

static_cast
妥当性について、コンパイル時のチェックはあるが、実行時のチェックは無し。生成されたアセンブリはunverifiable(検証不可)である。
dynamic_cast
実行時のチェックもある。不当なキャストには原則としてnullが返る(場合によってはstd::bad_cast例外が発生)。
const_cast
constを非constに変換するときに使う。実行時チェックは…?
reinterpret_cast
何でもキャスト可。
safe_cast
実行時チェックあり。生成されたアセンブリはverifiable(検証可能)である。不正なキャストにはSystem::InvalidCastException例外が発生する。

verifiableなアセンブリは、PEVerifyでチェックすることができます。PEVerifyは、アセンブリがタイプセーフ要件を満たしているかどうかをチェックするツールだそうです(よく分かりません)。コンパイルオプション/clr:safeを指定すると、コンパイラはverifiableなコードを生成しようとします。static_castを使ったコードを/clr:safeでコンパイルすると、(/clrや/clr:pureのときは問題ないようなケースでも)コンパイルエラーが発生するので、safe_castに変える必要があります。

ついでに、マネージ・コードを生成するときの/clrオプションについても簡単に整理しましょう。

オプション作用
/clrアンマネージ・コードやアンマネージ・データとの混在が可能。PInvokeかC++ Interopにより外部アセンブリを参照可能。CRT使用可(msvcm80.dll)。
/clr:pureアンマネージ・データとの混在が可能。PInvokeにより外部アセンブリを参照可能(C++ Interopは使用不可)。CRT使用可(msvcm80.dll)。
/clr:safeアンマネージとの混在は不可。PInvokeにより外部アセンブリを参照可能(C++ Interopは使用不可)。CRT使用不可。verifiableなアセンブリを生成。

キャストの実例

準備

下準備として、いくつかのCLIクラスとネイティブクラスを宣言します。BaseとDerivedには継承関係がありますが、Indivにはありません。同様に、NBとNDには継承関係があり、NIにはありません。コンパイルオプションは/clr:pureとします。

#include "stdafx.h"

using namespace System;
using namespace std;

ref class Base {
public:
  Base() {}
};

ref class Derived : public Base {
public:
  Derived() {}
};

ref class Indiv {
public:
  Indiv() {}
};

class NB {
public:
  NB() {}
  virtual ~NB() {}
  virtual void hello() { cout << "Hello NB" << endl; }
};

class ND : public NB {
public:
  ND() { count = 100; }
  virtual void hello() { cout << "Hello ND" << endl; }
  void helloForND() { cout << "Hollo ND, count=" << count << endl; }
private:
  int count;
};

class NI {
public:
  NI() {}
};

基本型

intをunsigned charへキャストしてみます。コンパイルエラーになる場合はコメントアウトしてます。

  int a = 10000;
  unsigned char b;

  b = a;  // C4244: データが失われる可能性あり。
  b = static_cast<unsigned char>(a);

// C2680: クラスへのポインタか参照じゃないとダメ。
// b = dynamic_cast<unsigned char>(a);

  b = safe_cast<unsigned char>(a);

static_castかsafe_castを使いましょう。

基本型へのポインタ

int*をunsigned char*へキャストします。

  int* c = &a;
  unsigned char* d;

// C2440:
// d = c;
// d = static_cast<unsigned char*>(c);

// C2680:
// d = dynamic_cast<unsigned char*>(c);

// C2682: 変換できない。
// d = safe_cast<unsigned char*>(c);

  d = reinterpret_cast<unsigned char*>(c);

ありそうなケースですが、reinterpret_castしか使えません。

CLIクラスのハンドル

DerivedオブジェクトをBase^で参照しておき、Derived^へダウンキャストします。また、強引にIndiv^へもキャストしてみましょう。

  Base^ aa = gcnew Derived();
  Derived^ bb;
  Indiv^ cc;

// C2440:
// bb = aa;

  bb = static_cast<Derived^>(aa);    // /clr:safeではC4957エラー。
  bb = dynamic_cast<Derived^>(aa);
  bb = safe_cast<Derived^>(aa);

// C2440:
// cc = static_cast<Indiv^>(aa);

  cc = dynamic_cast<Indiv^>(aa);     // nullptrが返る。

// C2682:
// cc = safe_cast<Indiv^>(aa);

  cc = reinterpret_cast<Indiv^>(aa);

dynamic_castかsafe_castですね。dynamic_castなら、キャストの成否(nullptr以外が返ったか)を確認する必要がありそうです。

次に、実体がBaseオブジェクトの場合を試してみます。

  aa = gcnew Base();

// C2440:
// bb = aa;

  bb = static_cast<Derived^>(aa);    // /clr:safeではC4957エラー。
  bb = dynamic_cast<Derived^>(aa);   // nullptrが返る。
  try {
    bb = safe_cast<Derived^>(aa);    // InvalidCastException発生。
  }
  catch ( InvalidCastException^ e ) {
    Console::WriteLine(e->Message);
       // 型 'Base' のオブジェクトを型 'Derived' にキャストできません。
  }

safe_castでは例外が発生しました。

ネイティブクラスのポインタ

まず、実体がNDオブジェクトの場合から。

  NB* aaa = new ND();
  ND* bbb;
  NI* ccc;

// C2440
// bbb = aaa;

  bbb = static_cast<ND*>(aaa);

  bbb = dynamic_cast<ND*>(aaa);      // NBがvtableを持たない場合、C2683エラー。

// C2452
// bbb = safe_cast<ND*>(aaa);

// C2440
// ccc = static_cast<NI*>(aaa);

  ccc = dynamic_cast<NI*>(aaa);      // NBがvtableを持たない場合、C2683エラー。
                                     // nullptrが返る。

// C2682
// ccc = safe_cast<NI*>(aaa);

  ccc = reinterpret_cast<NI*>(aaa);

次に、実体がNBオブジェクトの場合。

  aaa = new NB();

// C2440
// bbb = aaa;

  bbb = static_cast<ND*>(aaa);

  bbb = dynamic_cast<ND*>(aaa);      // NBがvtableを持たない場合、C2683エラー。
                                     // nullptrが返る。

// C2452
// bbb = safe_cast<ND*>(aaa);

ネイティブクラスの参照(&)

  NB& aaaa = *new ND();

// C2440
// ND& bbbb = aaaa;

  ND& bbbb1 = static_cast<ND&>(aaaa);
  ND& bbbb2 = dynamic_cast<ND&>(aaaa);
  ND& bbbb3 = safe_cast<ND&>(aaaa);

// C2440
// NI& cccc1 = static_cast<NI&>(aaaa);

  try {
    NI& cccc2 = dynamic_cast<NI&>(aaaa);   // bad_cast発生。
  }
  catch ( bad_cast& e ) {
    cerr << e.what() << endl;              // Bad dynamic_cast!
  }

// C2682
// NI& cccc3 = safe_cast<NI&>(aaaa);

  NI& cccc4 = reinterpret_cast<NI&>(aaaa);

dynamic_castでも、参照のキャストに失敗した場合は例外が発生します。受け側は参照なので、nullを返すわけにいきませんからね。

  NB& dddd = *new NB();

  ND& bbbb4 = static_cast<ND&>(dddd);

  try {
    ND& bbbb5 = dynamic_cast<ND&>(dddd);   // bad_cast発生。
  }
  catch ( bad_cast& e ) {
    cerr << e.what() << endl;              // Bad dynamic_cast!
  }

  ND& bbbb6 = safe_cast<ND&>(dddd);        // 成功!!
  bbbb6.hello();                           // Hello NB
  bbbb6.helloForND();                      // Hollo ND, count=-33686019

  delete aa;
}

なんと、safe_castで、強引なダウンキャストが成功してしまいました。多態なhello()では基本クラスの方が呼ばれてます。派生クラスのhelloForND()では、無いはずのフィールドを参照してしまってます。バグじゃないでしょうか。

Cスタイルのキャスト

最後に、Cスタイルのキャストについて。C++/CLIでも、(unsigned char)i みたいな従来のキャスト表記を使うことができます。その場合、コンパイラが以下の順序でマッチするもの探して自動適用してくれるそうです。

  1. const_cast
  2. safe_cast
  3. safe_cast plus const_cast
  4. static_cast
  5. static_cast plus const_cast
Last modified:2012/03/09 16:54:03
Keyword(s):
References:[.NETアプリ開発] [VC.NET ~ C++/CLIマイナスC++]
This page is frozen.