RTTIの薦め
RTTI は実行時型情報(RunTime Type Information)あるいは実行時型識別(RunTime Type Identification)と呼ばれる仕組みです
C++でこれをサポートするための構文が dynamic_cast です
今回はこのC++の dynamic_cast について復習しましょう
まず Fig.1 を見てください
Fig.1
class CSampleFoo { int m_foo; public: virtual int foo(){ return m_foo; } CSampleFoo( int foo ): m_foo(foo) { } virtual ~CSampleFoo(){ } }; class CSampleBar { int m_bar; public: virtual int bar(){ return m_bar; } CSampleBar( int bar ): m_bar(bar) { } virtual ~CSampleBar(){ } }; class CSampleFooBar : public CSampleFoo, public CSampleBar { public: CSampleFooBar( int foo, int bar ): CSampleFoo(foo), CSampleBar(bar) { } virtual ~CSampleFooBar(){ } };
CSampleFooBar は CSampleFoo と CSampleBar から派生しているので static_cast でも CSampleFooBar* から CSampleFoo* あるいは CSampleBar* へとキャストすることができます
使用例:
CSampleFooBar* pFooBar = new CSampleFooBar( 123, 456 ); printf( "pFooBar = 0x%p, foo = %3d, bar = %d\n", pFooBar, pFooBar->foo(), pFooBar->bar()); // pFooBar の型である CSampleFooBar は CSampleFoo と CSampleBar を基底クラスに持っているのでどちらの型へもキャストできる。 // static_cast でも dynamic_cast でも同じ。 CSampleFoo* pFoo = static_cast<CSampleFoo*>( pFooBar ); CSampleBar* pBar = static_cast<CSampleBar*>( pFooBar ); printf( "pFoo = 0x%p, foo = %3d\n", pFoo, pFoo ? pFoo->foo() : 0 ); printf( "pBar = 0x%p, bar = %3d\n", pBar, pBar ? pBar->bar() : 0 );
出力の例:
pFooBar = 0x01091B98, foo = 123, bar = 456 pFoo = 0x01091B98, foo = 123 pBar = 0x01091BA0, bar = 456
説明するまでもなくこれは当たり前のことですね
ところでここで pFoo と pBar は元々は同じインスタンスです
元が同じインスタンスなら pFoo から CSampleBar* を、あるいは逆に pBar から CSampleFoo* を取れてもよさそうなものだと思いませんか
しかし static_cast ではこれはできません
CSampleFoo と CSampleBar との間には何の関係もないのでこれも当たり前ですね
static_cast ではそれが当たり前なんですが、実は dynamic_cast を使うとそれができるようになります
使用例:
CSampleFooBar* pFooBar = new CSampleFooBar( 123, 456 ); printf( "pFooBar = 0x%p, foo = %3d, bar = %d\n", pFooBar, pFooBar->foo(), pFooBar->bar()); // pFooBar の型である CSampleFooBar は CSampleFoo と CSampleBar を基底クラスに持っているのでどちらの型へもキャストできる。 // static_cast でも dynamic_cast でも同じ。 CSampleFoo* pFoo = dynamic_cast<CSampleFoo*>( pFooBar ); CSampleBar* pBar = dynamic_cast<CSampleBar*>( pFooBar ); printf( "pFoo = 0x%p, foo = %3d\n", pFoo, pFoo ? pFoo->foo() : 0 ); printf( "pBar = 0x%p, bar = %3d\n", pBar, pBar ? pBar->bar() : 0 ); // pFoo の型である CSampleFoo と pBar の型である CSampleBar の間には何の関係もないので、 // static_cast だとコンパイルエラーになるけれども、 // dynamic_cast なら大本のインスタンス pFooBar の型である CSampleFooBar によって関係付けられているので相互にキャストできる。 CSampleFoo* pFoo2 = dynamic_cast<CSampleFoo*>( pBar ); CSampleBar* pBar2 = dynamic_cast<CSampleBar*>( pFoo ); printf( "pFoo2 = 0x%p, foo = %3d\n", pFoo2, pFoo2 ? pFoo2->foo() : 0 ); printf( "pBar2 = 0x%p, bar = %3d\n", pBar2, pBar2 ? pBar2->bar() : 0 );
出力の例:
pFooBar = 0x01091B98, foo = 123, bar = 456 pFoo = 0x01091B98, foo = 123 pBar = 0x01091BA0, bar = 456 pFoo2 = 0x01091B98, foo = 123 pBar2 = 0x01091BA0, bar = 456
簡単で便利ですね
このような使い方だと dynamic_cast も便利で単純な仕組みのようにみえるんですが、しかしここでは CSampleFoo と CSampleBar は基底クラスを持っていません
単純である理由はここにあるのです
ではこの2つのクラスが共通の基底クラスを持っていたらどうなるのでしょうか
すると途端に問題が複雑になります
まずは素直に共通の基底クラスを持つように書き換えてみるとそれは Fig.2 のようになるでしょうか
Fig.2
class CSampleUnknown { unsigned long m_cRef; public: virtual unsigned long AddRef(){ return ++m_cRef; } virtual unsigned long Release(){ if( --m_cRef == 0 ){ delete this; return 0; } return m_cRef; } CSampleUnknown(): m_cRef(1) { } virtual ~CSampleUnknown(){ } }; class CSampleFoo : public CSampleUnknown { int m_foo; public: virtual int foo(){ return m_foo; } CSampleFoo( int foo ): CSampleUnknown(), m_foo(foo) { } virtual ~CSampleFoo(){ } }; class CSampleBar : public CSampleUnknown { int m_bar; public: virtual int bar(){ return m_bar; } CSampleBar( int bar ): CSampleUnknown(), m_bar(bar) { } virtual ~CSampleBar(){ } }; class CSampleFooBar : public CSampleFoo, public CSampleBar { public: CSampleFooBar( int foo, int bar ): CSampleFoo(foo), CSampleBar(bar) { } virtual ~CSampleFooBar(){ } };
使用例:
CSampleFooBar* pFooBar = new CSampleFooBar( 123, 456 ); CSampleFoo* pFoo = dynamic_cast<CSampleFoo*>( pFooBar ); CSampleBar* pBar = dynamic_cast<CSampleBar*>( pFooBar ); CSampleFoo* pFoo2 = dynamic_cast<CSampleFoo*>( pBar ); CSampleBar* pBar2 = dynamic_cast<CSampleBar*>( pFoo ); CSampleUnknown* pUnknownFoo = dynamic_cast<CSampleUnknown*>( pFoo ); CSampleUnknown* pUnknownBar = dynamic_cast<CSampleUnknown*>( pBar ); printf( "pFooBar = 0x%p, foo = %3d, bar = %d\n", pFooBar, pFooBar->foo(), pFooBar->bar()); printf( "pFoo = 0x%p, foo = %3d\n", pFoo, pFoo ? pFoo->foo() : 0 ); printf( "pBar = 0x%p, bar = %3d\n", pBar, pBar ? pBar->bar() : 0 ); printf( "pFoo2 = 0x%p, foo = %3d\n", pFoo2, pFoo2 ? pFoo2->foo() : 0 ); printf( "pBar2 = 0x%p, bar = %3d\n", pBar2, pBar2 ? pBar2->bar() : 0 ); printf( "pUnknownFoo = 0x%p, pUnknownFoo->AddRef() = %d\n", pUnknownFoo, pUnknownFoo ? pUnknownFoo->AddRef() : 0 ); printf( "pUnknownBar = 0x%p, pUnknownBar->AddRef() = %d\n", pUnknownBar, pUnknownBar ? pUnknownBar->AddRef() : 0 );
出力の例:
pFooBar = 0x01091B98, foo = 123, bar = 456 pFoo = 0x01091B98, foo = 123 pBar = 0x01091BA0, bar = 456 pFoo2 = 0x01091B98, foo = 123 pBar2 = 0x01091BA0, bar = 456 pUnknownFoo = 0x01091B98, pUnknownFoo->AddRef() = 2 pUnknownBar = 0x01091BA0, pUnknownBar->AddRef() = 2 // あれ?
この Fig.2 のコードはコンパイルも通りますし実行してもエラーも出ないので一見これでよさそうですけれども、よくみるとおかしな点がありますね
お気づきでしょうか
「出力の例」のところです
pUnknownFoo->AddRef() と pUnknownBar->AddRef() がどちらも 2 を返していますね
これはつまり pUnknownFoo の持っているリファレンスカウンタと pUnknownBar の持っているリファレンスカウンタが別々のものだということです
でもおかしいですね。そもそも pUnknownFoo と pUnknownBar は元々同じ1つのインスタンス pFooBar なのですから、そのリファレンスカウンタは唯一つ pFooBar のリファレンスカウンタだけであるはずです
1つのカウンタに対して AddRef() を2度呼び出してそのカウンタに +1, +1 と操作したのなら、その結果は当然、2, 3 となる必要がありますね
この問題の原因は Fig.3 のように pFooBar->AddRef() を呼び出そうとしてみるとしてみるとコンパイラが教えてくれます
Fig.3
printf( "pFooBar->AddRef() = %d\n", pFooBar->AddRef());
それは「CSampleFooBar は CSampleFoo の実装している AddRef と CSampleBar の実装している AddRef の両方を持っているので、ただ AddRef と言われてもどちらの AddRef を呼び出せばいいのか分からない」という警告でしょう
どちらにしたらいいか決められないというのですから、これに対処する方法は2つありますね
1つは Fig.4 のように常にどちらかの基底クラスの実装だけを使う方法です
Fig.4
// CSampleFoo の方でも CSampleBar の方でもどちらでもいいけど、どちらか片方だけを使うこと。 class CSampleFooBar : public CSampleFoo, public CSampleBar { public: virtual unsigned long AddRef(){ return CSampleFoo::AddRef(); //return CSampleBar::AddRef(); } virtual unsigned long Release(){ return CSampleFoo::Release(); //return CSampleBar::Release(); } CSampleFooBar( int foo, int bar ): CSampleFoo(foo), CSampleBar(bar) { } virtual ~CSampleFooBar(){ } };
使用例:
CSampleFooBar* pFooBar = new CSampleFooBar( 123, 456 ); CSampleFoo* pFoo = dynamic_cast<CSampleFoo*>( pFooBar ); CSampleBar* pBar = dynamic_cast<CSampleBar*>( pFooBar ); CSampleUnknown* pUnknownFoo = dynamic_cast<CSampleUnknown*>( pFoo ); CSampleUnknown* pUnknownBar = dynamic_cast<CSampleUnknown*>( pBar ); printf( "pUnknownFoo = 0x%p, pUnknownFoo->AddRef() = %d\n", pUnknownFoo, pUnknownFoo ? pUnknownFoo->AddRef() : 0 ); printf( "pUnknownBar = 0x%p, pUnknownBar->AddRef() = %d\n", pUnknownBar, pUnknownBar ? pUnknownBar->AddRef() : 0 ); printf( "pFooBar->AddRef() = %d\n", pFooBar->AddRef());
出力の例:
pUnknownFoo = 0x01091B98, pUnknownFoo->AddRef() = 2 pUnknownBar = 0x01091BA0, pUnknownBar->AddRef() = 3 pFooBar->AddRef() = 4
こうすることで Fig.3 はコンパイルを通るようになりますし、リファレンスカウンタも常に片方だけを使うので1つになります
しかしこの方法は一見よさそうなのですけれども Fig.5 のように pFooBar を CSampleUnknown* にキャストしようとしてみるとまた警告が出てしまうでしょう
Fig.5
CSampleUnknown* pUnknown = dynamic_cast<CSampleUnknown*>( pFooBar );
それは「ただ CSampleUnknown と言われてもそれが CSampleFoo の基底クラスとなっている CSampleUnknown なのか、それとも CSampleBar の基底クラスとなっている CSampleUnknown なのか決められない」という警告でしょうか
C++にはこの問題を回避するための仕組みも提供されています
そして先程言ったもう1つの方法というのもその仕組みを使う方法です
それは「仮想継承」を使う方法です
「仮想継承」を使うと継承ツリーの中にある共通の基底クラスを1つにまとめてもらえます
「基底クラスをどちらにしたらいいか決められない」というのですから、迷わなくていいように1つにまとめてしまえばいいわけですね
Fig.6
class CSampleUnknown { unsigned long m_cRef; public: virtual unsigned long AddRef(){ return ++m_cRef; } virtual unsigned long Release(){ if( --m_cRef == 0 ){ delete this; return 0; } return m_cRef; } CSampleUnknown(): m_cRef(1) { } virtual ~CSampleUnknown(){ } }; class CSampleFoo : virtual public CSampleUnknown { int m_foo; public: virtual int foo(){ return m_foo; } CSampleFoo( int foo ): CSampleUnknown(), m_foo(foo) { } virtual ~CSampleFoo(){ } }; class CSampleBar : virtual public CSampleUnknown { int m_bar; public: virtual int bar(){ return m_bar; } CSampleBar( int bar ): CSampleUnknown(), m_bar(bar) { } virtual ~CSampleBar(){ } }; class CSampleFooBar : virtual public CSampleFoo, virtual public CSampleBar { public: CSampleFooBar( int foo, int bar ): CSampleFoo(foo), CSampleBar(bar) { } virtual ~CSampleFooBar(){ } };
使用例:
CSampleFooBar* pFooBar = new CSampleFooBar( 123, 456 ); CSampleFoo* pFoo = dynamic_cast<CSampleFoo*>( pFooBar ); CSampleBar* pBar = dynamic_cast<CSampleBar*>( pFooBar ); CSampleFoo* pFoo2 = dynamic_cast<CSampleFoo*>( pBar ); CSampleBar* pBar2 = dynamic_cast<CSampleBar*>( pFoo ); CSampleUnknown* pUnknownFoo = dynamic_cast<CSampleUnknown*>( pFoo ); CSampleUnknown* pUnknownBar = dynamic_cast<CSampleUnknown*>( pBar ); printf( "pFooBar = 0x%p, foo = %3d, bar = %d\n", pFooBar, pFooBar->foo(), pFooBar->bar()); printf( "pFoo = 0x%p, foo = %3d\n", pFoo, pFoo ? pFoo->foo() : 0 ); printf( "pBar = 0x%p, bar = %3d\n", pBar, pBar ? pBar->bar() : 0 ); printf( "pFoo2 = 0x%p, foo = %3d\n", pFoo2, pFoo2 ? pFoo2->foo() : 0 ); printf( "pBar2 = 0x%p, bar = %3d\n", pBar2, pBar2 ? pBar2->bar() : 0 ); printf( "pUnknownFoo = 0x%p, pUnknownFoo->AddRef() = %d\n", pUnknownFoo, pUnknownFoo ? pUnknownFoo->AddRef() : 0 ); printf( "pUnknownBar = 0x%p, pUnknownBar->AddRef() = %d\n", pUnknownBar, pUnknownBar ? pUnknownBar->AddRef() : 0 ); // 共通の基底クラスが1つにまとまっているので、迷わずキャストもできる。 CSampleUnknown* pUnknown = dynamic_cast<CSampleUnknown*>( pFooBar ); printf( "pUnknown = 0x%p, pUnknown->AddRef() = %d\n", pUnknown, pUnknown ? pUnknown->AddRef() : 0 ); // 共通の基底クラスが1つにまとまっているので、派生クラス側で AddRef を書かなくても使える。 printf( "pFooBar->AddRef() = %d\n", pFooBar->AddRef());
出力の例:
pFooBar = 0x01091B98, foo = 123, bar = 456 pFoo = 0x01091BA4, foo = 123 pBar = 0x01091BB0, bar = 456 pFoo2 = 0x01091BA4, foo = 123 pBar2 = 0x01091BB0, bar = 456 pUnknownFoo = 0x01091B9C, pUnknownFoo->AddRef() = 2 pUnknownBar = 0x01091B9C, pUnknownBar->AddRef() = 3 pUnknown = 0x01091B9C, pUnknown->AddRef() = 4 pFooBar->AddRef() = 5
さて、初めは単純に見えていたコードも CSampleFooBar の基底クラス CSampleFoo と CSampleBar に共通の基底クラスを持たせるようにしただけで「仮想継承」なんていう新たな仕組みまで出てきてやけに複雑なことになってしまいました
しかし考え様によっては、見た目上はクラス定義の基底クラスの前に virtual を付けるだけなのですから、別に複雑でもなんでもなく簡単でしょうか
どちらにせよ RTTI を使うときには一緒に「仮想継承」もというのは覚えておきましょう
では今回はこの辺で