C++には仮想継承という仕組みがある。 使ったこともこれから使う予定も、それ以前にC++自体を使う機会も乏しい実情。 しかしながらどうやって実現されているのかには興味があるので調べてみた。
環境:
$ g++ --version |
仮想継承とは?
そもそも「仮想継承」とはなにか? C++では仮想関数という仕組みがあってそちらはオブジェクト指向言語の基本機能として日常的に使われる(ポリモーフィズム)と思うが、それとは異なり仮想継承というのはほとんど見たことも使ったこともない代物だと思う。
仮想継承はC++特有の実装上の機能で、多重継承で発生する「ダイアモンド継承」での問題を解決するための仕組み。
同じベースクラスから派生している複数の親を多重継承させた場合、ベースクラスの実体が複数できてしまって齟齬が発生してしまう。 このケースで通常の継承の代わりに仮想継承を使うことでベースクラスの実体が1つに統合されて、重複されることにより起こる問題が解決できる:
|
上記で、DiamondインスタンスでDerive1側のsetOn1メソッド経由でBase::baseに値をセットしても、Derive2側のreferOn2メソッドで取り出すとセットされてない、ということが起こる(別の実体だから)。
これをpublic Baseという継承をvirtual public Baseに変更すれば解決できる。
多重継承しているDiamondクラスではなく、単一継承のDeive1や2に仮想継承を指定する必要があるのが注意な点。
そもそも多重継承やダイアモンド継承をすること自体がプログラム構成的に問題ある気がせんでもないが、まあそれはそれとして。
動作的にはBaseクラスの実体を重複させたい場合なんてないだろうから継承のデフォルトを仮想継承にすればいいじゃんと思うが、速度的なデメリットのため副次的な扱いになってるんでしょう。
仮想継承した場合のメモリレイアウト
通常の継承の場合、親クラスの実体が子クラスの先頭に配置され、その後に子クラスのフィールドが配置される。 多重継承させた場合も順に配置される。
仮想継承でベースクラスが共有されるのはどうやって実現されているか? ポインタ値をダンプしてメモリレイアウトを見てみる:
+4 +0 |
多重継承しているDiamondは親クラスのDerive1と2を順に配置、その後に自分自身のメンバ変数が配置される(あれば)。
最後に仮想基底クラスの実体が配置される。
Derive1や2は仮想継承の親クラスBaseの実体が直接配置されるのではなく、自分自身の仮想関数テーブルが置かれる
(仮想関数がなくても自動的に生成されるっぽい)。
仮想関数テーブルには型情報が含まれていて、親クラスの実体へのオフセットが得られる。
例えばDiamondのインスタンスにおけるDerive1の型情報のBase親クラスへのオフセットは+16 (=16-0)、
Derive2の型情報のオフセットは+8 (=16-8)となる。
仮想継承した場合の親クラスアクセス
上記のメモリレイアウト通り、仮想継承した場合の親クラスへのアクセスは親インスタンスのポインタ計算に仮想関数テーブルの参照が挟まることになる。 そのため通常の継承の場合のオフセットがコンパイル時に計算できるのと違って、実行時に一段メモリアクセスが入るので僅かながらペナルティがかかることになる。
DiamondのインスタンスからBaseへのアクセスも仮想継承を経由するので、同様の処理が行われる。
no-rtti:可
前述の通り仮想継承を使用すると仮想関数テーブルに紐づいた型情報が使用される。
これを型情報を無効にするコンパイラオプション-fno-rttiを指定したらどうなるか?
結果としては問題なくコンパイル・実行できる。
typeidに関わる情報は出力されないっぽいが継承関係に関わる情報は出力されるっぽく、仮想継承は問題なく動作する。
static_cast:利用不可
以前static_castとかを調べた時は通常の継承しか考慮してなかった。 仮想継承を使用している場合はどうなるのかと思ったらそもそもコンパイルエラーだった:
Base* b = ...; // なんらかのベースポインタ |
dynamic_castを使用する必要がある。