C ++、Cスタイルのキャスト、およびそれらを組み合わせた場合の予期しない結果

C ++は、Cから型キャスト(タイプ)(キャスト対象)-通常はCキャストと呼ばれます。C++には、さらに4つの明示的なキャスト-static_cast、reinterpret_cast、dynamic_cast、const_castがあります。



C ++は最新の言語ではなく、どちらが良いかについての激しい議論があります。Cのスタイルでキャストするか、適切な組み合わせで* _castを使用するかは、かなり前から始まり、今日に至っていません。 火に燃料を追加することはしません。例を考えて、誰もが一番好きなものを決定できるようにします。



ここではWindows固有の構造とCOMテクノロジについて説明しますが、型変換に十分な注意を払わないと、かなり複雑なクラス階層でも同じ問題が発生する可能性があります。



実際のオープンソースプロジェクトからの実際のコードに基づいた例。 プロジェクトの特定のサブシステムでは、いくつかのCOMインターフェイスを実装するクラスが宣言されています。



 class CInterfacesImplementor : public IComInterface1, public IComInterface2, public IComInterface3, ...(   4  9), public IComInterface10 { //    };
      
      











もちろん、実際には、インターフェイスにはより意味のある名前が付いていますが、十数個に近づくと、これは後で説明する問題の助けにはなりません。



各COMインターフェイスはIUnknownから直接または間接的に継承され、IUnknownにはQueryInterface()メソッドが含まれていることを思い出してください。その正しい実装は非常に複雑であるため、Raymond Chenがこのシリーズについて記述しました( hereherehere )。



この例は、上記のクラスのQueryInterface()の単なる実装です。 簡単な背景:開発者が新しいCOMインターフェイスを発表するとき、彼は一意の識別子を割り当てる義務があります。 呼び出し元はQueryInterface()を呼び出して、オブジェクトがそのような識別子を持つインターフェイスを実装しているかどうかを確認し、実装している場合は、対応する型のポインターを取得します。 __uuidof()コンストラクトは、コンパイル時にかっこで示されたインターフェイスの識別子を見つけて置き換えるようにVisual C ++に要求します。



だから...

 HRESULT STDMETHODCALLTYPE CInterfaceImplementor::QueryInterface( REFIID iid, void** ppv ) { if( ppv == 0 ) { return E_POINTER; } if( iid == __uuidof( IUnknown ) || iid == __uuidof( IComInterface1 ) ) { *ppv = (IComInterface1*)this; } else if( iid == __uuidof( IComInterface2 ) ) { *ppv = (IComInterface2*)this; } else if( iid == __uuidof( IComInterface3 ) ) { *ppv = (IComInterface3*)this; } else if... ... //       COM- } else { //   COM-     *ppv = 0; return E_NOINTERFACE; } AddRef(); return S_OK; }
      
      







上記の実装は機能し、ほぼ完璧です。 彼女は逆参照する前にポインターをチェックします。 彼女は、既知のインターフェースが要求されたかどうかを確認します。 E_NOINTERFACEコードを返す前にヌルポインターを書き込みます。 インターフェイスがサポートされている場合、参照カウントが増加します。 彼女はIUnknownリクエストにも正しく応答します。 レイモンド・チェンは、1つの質問がなければ喜んでいます。



なぜ幽霊がいるのですか? 「* ppv = this;」と書いてみませんか?



多重継承を使用すると、オブジェクトは基本クラスのサブオブジェクトで「構成」されるため、各サブオブジェクトに個別にアクセスできます。 たとえば、一部の関数はIComInterface2 *でのみ機能します-派生オブジェクトではなく、この特定のサブオブジェクトへのポインターを渡す必要があります。



「* ppv = this;」を割り当てると、派生オブジェクトの開始アドレスが毎回送信され、サブオブジェクトではありません。 別のサブオブジェクトへのポインターを介して仮想インターフェイスメソッドを呼び出そうとすると、明らかに長いデバッグにつながります。



上記の例では、ポインタの調整のみを提供しています。 呼び出し元が目的のサブオブジェクトへのポインタを受け取るために必要です。



幸せはありますか? この段落の前-確かに。 現在、100500日が経過し、プロジェクトが開発され、新しい機能が追加されています。 次の段落では、プロジェクトを開発しようとするときに、コピーと貼り付けの使用が失敗した場合の結果を確認します。 「適切なプログラミング」と「適切なアーキテクチャ」を備えた「適切なプログラマ」はおそらくそうではないという異議を省きましょう。



同じオープンソースプロジェクトの別のサブシステムには、同じインターフェイスセットを実装する別のクラスがあります。

 class CYetOtherImplementor : public IComInterface1, public IComInterface3, ...(   4  9), public IComInterface10 { //    };
      
      





そして、当然、実装が明らかに同じであるため、条件チェーンを新たに書きたくはありません。

 HRESULT STDMETHODCALLTYPE CYetOtherImplementor::QueryInterface( REFIID iid, void** ppv ) { if( ppv == 0 ) { return E_POINTER; } if( iid == __uuidof( IUnknown ) || iid == __uuidof( IComInterface1 ) ) { *ppv = (IComInterface1*)this; } else if( iid == __uuidof( IComInterface2 ) ) { *ppv = (IComInterface2*)this; } else if( iid == __uuidof( IComInterface3 ) ) { *ppv = (IComInterface3*)this; } else if... ... //       COM- } else { //   COM-     *ppv = 0; return E_NOINTERFACE; } // V2UncmUgaGlyaW5nIC0gd3d3LmFiYnl5LnJ1L3ZhY2FuY3k= AddRef(); return S_OK; }
      
      





IComInterface2インターフェースが要求されたときに何が起こるかを精神的に失います。 コントロールは、識別子が一致するまでif-else-ifチェーンに従い、Cスタイルのキャストが実行されます。



C ++標準ISO / IEC 14882:2003(E)のセクション5.3.5 / 5では、Cスタイルでキャストする場合、static_castが実行される(この場合)か、static_castが不可能な場合はreinterpret_castが実行されます。



最初の例では、クラスはIComInterface2から継承され、thisポインターのstatic_castが目的のサブオブジェクトへのポインターに実行されました。



2番目の例では、クラスはIComInterface2から継承されないため(yes、コピーと貼り付け、ファイルリビジョン)、したがってstatic_castは使用できません。 Reinterpret_castが実行され、thisポインターは変更されずにコピーされます。 ところで、オブジェクトはIComInterface2をまったく実装していません。 ここでは「突然」という言葉が適切です。



呼び出し元は、2番目の例でIComInterface2を要求すると、このインターフェイスが実装せず、通常このインターフェイスに適用しないオブジェクトへのゼロ以外のポインターを受け取ります。



比較のために、各if-else-ifブランチでstatic_castを使用すると、コンパイラはエラーメッセージを表示し、2番目の例はコンパイルされません。これにより、ファイルをもう少し処理する必要があることが開発者に優しく示唆されます。 デバッグ日が少なくて済むので、何か便利なことができます。



ここにいるので、別の悪いアイデアはdynamic_castを使用することです。 2番目の例でdynamic_castを使用する場合、呼び出し元はメソッドの実行が成功したことを示すヌルポインターと偽のコードを受け取り、オブジェクトがリークする可能性のある参照カウンターの増加を無駄に引き起こします。 さらに、数時間のデバッグが行われますが、nullポインターは少なくとも気づきやすいですが、ここでdynamic_castを使用する意味はまったくありません。



Cスタイルのゴーストを使用すると、コードを短く書くことができますが、正しいコードの記述が複雑になり、ゴースト* _castに慣れる必要があるときだけ延期することができます。



結論は明らかです。 Cスタイルのゴーストをできるだけ頻繁に使用します。これにより、他の開発者に競争上の優位性を与えることができ、いつかダーウィン賞を受賞することもできます。



ドミトリー・メッシェリャコフ

データ入力製品部門



All Articles