キャスト表記と safe_cast<> の導入

キャスト表記は、Visual C++ 2010 では C++ マネージ拡張から変更されています。

既存の体系に変更を加えることは、新しい体系を最初から作成するのとは異なる、より困難な作業です。 既存の体系への依存もあり、自由度が低いうえに、理想と現実の狭間で妥協が伴うことも少なくありません。

言語拡張機能も同様です。 1990 年代初頭、オブジェクト指向プログラミングが重要なパラダイムとなると、C++ においてタイプ セーフなダウンキャスト機能の必要性に迫られました。 ダウンキャストは、基本クラスのポインターまたは参照を派生クラスのポインターまたは参照へと、ユーザーに対して明示的に変換することです。 ダウンキャストには、明示的な型変換が要求されます。 問題は、実行時の側面の 1 つが基本クラスのポインターの実際の型であるため、コンパイラではチェックできない点にあります。 また、ダウンキャスト機能は仮想関数呼び出しと同じように動的な解決のフォームを必要とします。 ここで、2 つの疑問が発生します。

  • ダウンキャストがオブジェクト指向パラダイムに必要になる理由は何でしょうか。 なぜ仮想関数では不十分なのでしょうか。 つまり、ダウンキャスト (またはなんらかのキャスト) の必要性が、単純に設計ミスとは言えないのはなぜでしょうか。

  • C++ でダウンキャストのサポートが問題になる必要があるのはなぜでしょうか。 Smalltalk (または Java や C#) などのオブジェクト指向言語では問題になりませんでした。 C++ でダウンキャスト機能のサポートが困難な理由は何でしょうか。

仮想関数は、型のファミリによって異なる振る舞いをする関数です (ここではインターフェイスについては触れません。インターフェイスは、ISO-C++ ではサポートされませんが、CLR プログラミングではサポートされているため、使い方によっては、仮想関数に代わる手段として利用できます)。 このファミリのデザインは、通常、共通インターフェイス (仮想関数) を宣言する抽象基本クラスが存在するクラス階層構造とアプリケーション ドメインで実際のファミリ型を表す具体的な派生クラスのセットで表されます。

たとえば、CGI (Computer Generated Imagery) アプリケーション ドメインの Light 階層構造には、color、intensity、position、on、off などの共通属性があります。 共通のインターフェイスを使用することにより、特定の光がスポットライト、指向性ライト、非指向性ライト (太陽など)、またはバーンドア ライトであるかに煩わされることなく、さまざまな光を制御できます。 この場合、特定のライトの型にダウンキャストして、あえて仮想インターフェイスを使用する必要はありません。 しかし、稼動環境では、速度が重要な要素となってきます。 各メソッドをダウンキャストし、明示的に呼び出すことで、仮想機構を経由する代わりに呼び出しのインライン実行を実行できるのであれば、この方法を選択することも可能です。

したがって、C++ でダウンキャストを使用する理由は、仮想機構を抑制し、実行時のパフォーマンスを最大限に高めることと言えます (この手動最適化の自動化は研究の活発な領域です。 ただし、register または inline キーワードの明示的な使用を置き換えるより解決が困難です)。

ダウンキャストを行う第 2 の理由は、ポリモーフィズムの二重性にあります。 ポリモーフィズムを考慮する場合、フォームを受動的と動的のペアに分ける方法があります。

仮想呼び出し (およびダウンキャスト機能) はポリモーフィズムの動的な使用に相当します。ここでは、プログラム実行に含まれる特定のインスタンスにおける基本クラスのポインターの実際の型に基づく操作が実行されます。

一方、派生クラスのオブジェクトをその基本クラスのポインターに割り当てることはポリモーフィズムの受動的なフォームです。ポリモーフィズムは置換機構として使用されます。 これは、たとえば、汎用前 CLR プログラミングにおける Object オブジェクトの主な用途です。 受動的に使用すると、置換およびストレージに対して選択された基本クラスのポインターによって抽象的すぎるインターフェイスが得られる場合が一般的です。 たとえば、Object ではそのインターフェイスを通じて 5 つのメソッドが提供されます。動作をより明確化するには明示的なダウンキャストが必要になります。 たとえば、スポットライトの角度やスポットライトが消える速度を調節する場合は、明示的なダウンキャストが必要になります。 サブタイプのファミリ内の仮想インターフェイスをその多数の子の考えうるすべてのメソッドのスーパーセットになることは実行不可能です。ダウンキャスト機能がオブジェクト指向言語で必ず必要になるのはこのためです。

オブジェクト指向言語に安全なダウンキャスト機能が必要なのであれば、C++ にこの機能が追加されるまでこれだけの長い時間を要したのはなぜでしょうか。 問題は、ポインターのランタイム型に関する情報を使用可能にする方法です。 仮想関数の場合、コンパイラは、ランタイム情報を 2 つに分けてセットアップします。

  • クラス オブジェクトは、通常のメンバーとは別に、適切な仮想テーブルに到達するための仮想テーブル ポインター メンバーを、そのクラス オブジェクトの先頭または最後に持ちます (この点については、それ自体、興味深い経緯があります)。 たとえば、スポットライト オブジェクトは、まず、スポットライトの仮想テーブルを突き止め、そこから、指向性ライト、指向性ライトの仮想テーブル、というようにたどっていきます。

  • このテーブルには、仮想関数ごとに固定のスロットが関連付けられており、実際に呼び出すインスタンスは、このテーブルに格納されているアドレスで表されます。 たとえば、仮想 Light デストラクターがスロット 0 に関連付けられたり、Color がスロット 1 に関連付けられたりします。 これは、コンパイル時に設定され、オーバーヘッドが最小のため、柔軟性のない戦略時に効率的です。

問題は、第 2 のアドレスを追加するか、または型エンコーディングの一種を直接追加して、C++ のサイズを変更せずに型情報をポインターに使用可能にする方法です。 これは、依然として支配的なユーザー コミュニティであるオブジェクト指向パラダイムを使用しないプログラマ (およびプログラム) には受け入れられません。 ポリモーフィック クラス型に特殊なポインターを導入することも考えられましたが、これは紛らわしく、特にポインター演算の問題で、2 つを混合することが困難になります。 各ポインターをその現在の関連型に関連付けているランタイム テーブルを維持することも、動的に更新することも許されません。

次の問題は、異なってはいるが、プログラムに対して合理的な願望を持っている 2 つのユーザー コミュニティの組み合わせです。 ソリューションは、その願望と相互運用性を維持する一方で、この 2 つのコミュニティの意見の折衷案である必要があります。 これは、いずれかの側が主張するソリューションが実行不可能になる可能性が高く、さらに最終的に実装するソリューションが不完全になる可能性が高くなることを意味しています。 実際の解決では、ポリモーフィック クラスの定義を中心に展開しました。ポリモーフィック クラスは仮想関数を含むクラスです。 ポリモーフィック クラスは動的でタイプ セーフなダウンキャストをサポートします。 すべてのポリモーフィック クラスにはその関連仮想テーブルにこの追加ポインター メンバーが格納されるため、ポインターをアドレスで維持するという問題が解決されます。 このため、関連の型情報を拡張された仮想テーブル構造に格納できます。 タイプ セーフなダウンキャストのコストは、この機能のユーザーに (その大部分が) ローカライズされます。

タイプ セーフなダウンキャストに関する次の問題は、その構文です。 これはキャストのため、ISO-C++ 委員会への元の提案では、次に示すような、飾りのないキャスト構文が使用されていました。

spot = ( SpotLight* ) plight;

しかし、このままではユーザーがキャストのコストを管理できないとして委員会に却下されました。 動的でタイプ セーフなダウンキャストが、以前は安全でなかったが静的なキャスト表記であった構文と同じ構文を備えた場合、代替になり、不要でコストがかかりすぎる場合でもユーザーはランタイム オーバーヘッドを抑えることができません。

通常、C++ には、コンパイラがサポートする機能を抑制する機構が存在します。 たとえば、クラス スコープ演算子 (Box::rotate(angle)) を使用するか、(クラスのポインターや参照ではなく) クラス オブジェクト経由で仮想メソッドを呼び出すことにより、仮想機構を無効にできます。 後者の抑制方法は、言語的な要件というより、実装の質の問題です。一時変数が構築されるのを、次のような形式で宣言することによって抑制することと似ています。

// compilers are free to optimize away the temporary
X x = X::X( 10 );

こうして、この提案についてさらに検討することになり、いくつかの代替表記が検討されました。委員会に戻された表記に (?type) というフォームがありました。これは未決定、つまり動的な性質を示しています。 これを使用することで、ユーザーは 2 つのフォーム (静的または動的) を切り替えることができますが、だれからもあまり好まれませんでした。 こうして、さらなる検討が進められました。 3 番目の成功を収めた表記は現在標準の dynamic_cast<type> で、4 つの新しいスタイルのキャスト表記に一般化されました。

ISO-C++ では、dynamic_cast が不適切なポインター型に適用されると 0 が返されます。また、参照型に適用されると std::bad_cast 例外がスローされます。 C++ のマネージ拡張では、マネージ参照型に dynamic_cast を適用すると (ポインター表現のため)、常に 0 が返されていました。 そこで、dynamic_cast の例外をスローするための同様の手段として、__try_cast<type> が導入されました。従来と異なるのは、キャストが失敗したときに、System::InvalidCastException がスローされる点です。

public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
   ItemVerb *EnsureVerbArray() [] {
      return __try_cast<ItemVerb *[]>
         (verbList->ToArray(__typeof(ItemVerb *)));
   }
};

新しい構文では、__try_cast が safe_cast として再キャストされました。 次に、新しい構文におけるコード片を示します。

public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
   array<ItemVerb^>^ EnsureVerbArray() {
      return safe_cast<array<ItemVerb^>^>
         ( verbList->ToArray( ItemVerb::typeid ));
   }
};

マネージの世界では、コードを検証不可能にしたまま型間にキャストするプログラマの能力を制限して検証可能なコードを許容することが重要です。 これは、新しい構文の動的プログラミング パラダイムの重要な側面です。 このため、旧式のキャストのインスタンスは、ランタイム キャストとして次のように内部で再キャストされます。

// internally recast into the 
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid ); 

一方、ポリモーフィズムによりアクティブ モードとパッシブ モードの両方が実現したため、サブタイプの非仮想 API へのアクセスを得るためだけにダウンキャストを行うことが必要になることがあります。 これは、クラスのメンバーが階層構造内に任意の型をアドレス指定する (トランスポート機構の受動的なポリモーフィズム) ことを望み、特定のプログラム コンテキスト内の実際のインスタンスがわかっている場合に起こることがあります。 この場合、キャストのランタイム チェックには、受け入れがたいオーバーヘッドが伴います。 新しい構文がマネージ システム プログラミング言語として機能するには、コンパイル時間 (つまり静的) ダウンキャストを行う方法を提供する必要があります。 static_cast 表記を適用してコンパイル時間ダウンキャストを行うことができるようにしているのはこのためです。

// ok: cast performed at compile-time. 
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));

問題は、static_cast を行うプログラマがこれを正しく、善意から行うことを保証する手段がなく、マネージ コードが検証可能であることを強制する手段がないことです。 これは動的プログラム パラダイム下ではネイティブ下でより切迫した問題となりますが、システム プログラミング言語で静的キャストとランタイム キャストを切り替える機能をユーザーに認めないだけでは不十分です。

ただし、新しい構文のパフォーマンスの点で注意すべき点があります。 ネイティブなプログラミングでは、旧式のキャスト表記と新しい static_cast 表記にパフォーマンスの点で違いはありません。 しかし、新しい構文では、旧式のキャスト表記は新しい static_cast 表記を使用した場合より非常にコストがかかります。 なぜなら、旧式の表記は、例外をスローするランタイム チェックへと、コンパイラによって内部的に変換されるためです。 さらに、アプリケーションを停止させる未知の例外が発生することになるため、コードの実行プロファイルも変更されます。しかし、static_cast 表記を使用した場合、同じエラーがこの例外を生じることはありません。 これがユーザーを新しい表記に駆り立てる助けになると考える人もいる可能性があります。 しかし、エラーが発生した場合に限られますが、旧式の表記を使用するプログラムの速度が著しく低速化してしまい、その理由も明らかになりません。これは、次の C プログラマの落とし穴に似通っています。

// pitfall # 1: 
// initialization can remove a temporary class object, 
// assignment cannot
Matrix m;
m = another_matrix;

// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;

参照

参照

C-Style Casts with /clr

safe_cast

概念

言語の変更の概要