多重継承な場合の、メモリ配置について

A、B二つのクラスを継承したCというクラスを、A、Bにキャスト、あるいはそこからさらにアップキャストしたらどうなるかを実験してみた。あくまで、手元のVS2008SP1で実験してみた結果。

#include <iostream>
using namespace std;
#if 01
class A{ public: char m_a[5]; };
class B{ public: char m_b[3]; };
class C: public A, public B{};
class D: public B, public A{};// A,Bを継承する順番が、Cと逆!
size_t take_B( B* b ){	return sizeof( *b ); }
size_t take_A( A* a ){ return sizeof( *a ); }
void main( void )
{
	C* c = new C;
	D* d = new D;
	cout<<"C(:public A, public B) *c = new C:\t" << c<<endl;
	cout<<"static_cast<A*>(c)\t\t\t" << static_cast<A*>(c) <<endl;
	cout<<"static_cast<B*>(c)\t\t\t" << static_cast<B*>(c)<<endl;
	cout<<"static_cast<A*>(static_cast<void*>(c));\t" 
		<< static_cast<A*>(static_cast<void*>(c))<<endl;
	cout<<"static_cast<B*>(static_cast<void*>(c));\t" 
		<< static_cast<B*>(static_cast<void*>(c))<<endl;
	cout<<"static_cast<C*>(static_cast<B*>(c))\t"
		<< static_cast<C*>(static_cast<B*>(c)) <<endl;
	cout<<"static_cast<C*>(static_cast<A*>(c))\t"
		<< static_cast<C*>(static_cast<A*>(c)) <<endl;
	cout<<"static_cast<C*>(static_cast<B*>(static_cast<void*>(c)));->" 
		<< static_cast<C*>(static_cast<B*>(static_cast<void*>(c)))<endl<<endl;
	cout<<"D(:public B, public A) *d = new D:\t" << d<<endl;
	cout<<"static_cast<A*>(d)\t\t\t" << static_cast<A*>(d) <<endl;
	cout<<"static_cast<B*>(d)\t\t\t" << static_cast<B*>(d)<<endl;
	int aa=0;
	return;
}

結果はこうなった。

C(:public A, public B*) *c = new C:     00000000002E7F00
static_cast<A*>(c)                      00000000002E7F00
static_cast<B*>(c)                      00000000002E7F05
static_cast<A*>(static_cast<void*>(c)); 00000000002E7F00
static_cast<B*>(static_cast<void*>(c)); 00000000002E7F00
static_cast<C*>(static_cast<B*>(c))     00000000002E7F00
static_cast<C*>(static_cast<A*>(c))     00000000002E7F00
static_cast<C*>(static_cast<B*>(static_cast<void*>(c)));->00000000002E7EFB

D(:public B, public A*) *d = new D:     00000000002E7F50
static_cast<A*>(d)                      00000000002E7F53
static_cast<B*>(d)                      00000000002E7F50

A*が、Cの定義で一番最初に定義されているならば、A*のポインタを持っているなら、それはC*の先頭ポインタでもある。しかし、A*が、Cの定義で、二番目以降に定義されていた場合は、本体の実体である、C*の先頭アドレスとは異なる。(結果表示の3行目、および最下部のDの場合、参照)
本来実体としては、CとかDなんだけれども、基底のA*やB*のポインタとしてしか、それが得られない場合、CとかDの型が見えているのなら、A*,B*どちらのポインタを持っていても、static_castでCとかDの実体の先頭アドレスを正しく得ることができる。何故なら、CとかDはその型の情報として、その中におけるA,Bの配置関係を持っているから、ポインタは正しいバイト数だけ適切にオフセットされる。しかし、CとかDの型が見えない場合はどうだろうか。
A*のポインタを持っている場所で、CとかDの型が見えればよいが、派生型が見えない基底Aしか扱わないライブラリとかだったりした場合、例えばその中でのオブジェクト(CとかDとしての実体全体)のコピーは、基本的には絶望的である。ただし、CとかDの型が見えない環境でも、もし、オブジェクト実体(CとかD)のサイズだけはわかっており、かつ、Aが、CとかDの定義において一番最初に継承されていることが約束されているならば、オブジェクトの(CとかDとしての実体全体)コピーは、かろうじて可能だ。
なので、Aのみを扱う基底のアルゴリズムなどを使う場合、CとかDのような多重継承クラスとして利用する場合があるのなら、それはAを一番最初に継承する形で、定義しておけば、そのアルゴリズムに、実体のサイズ(Cのサイズ)さえ渡せば、オブジェクトのメモリ空間内での移動、再配置やコピーを、ライブラリの機能として実装することが可能といえば可能だ。
Cの型が見えない以上、もしAが一番最初に継承されない場合は、つまりオブジェクト実体の先頭アドレスを知る術は、もはや完全にないので、オブジェクトの再配置処理はAだけ扱う基底ライブラリの中の処理では事実上無理となる。
また、逆に、派生オブジェクトの先頭アドレスがわかっていますと、そしてオブジェクトのサイズもわかっています。しかし派生オブジェクトの型が相変わらずわかりません。みたいな状況だと、オブジェクト自体の再配置みたいなことはできるわけだけれども、そのオブジェクトの中の、どこに、A*があったものかがわからない、それが知りようがないために、再配置したはいいけど、A*としては、再配置する前の場所、再配置した後の場所、それぞれ、Aのポインタ値としてはどこからどこへ行ったのかということを、記録しておくことができない。なので、結局再配置先の派生オブジェクトをA*として活用することは、基底ライブラリ内では結局できないことになる。
なんだけれども、A*が一番最初に継承宣言された基底クラスであることが約束されているならば、派生オブジェクトの先頭アドレスがそのままA*としての場所でもある、という事実が利用できるのである。
以上、多重継承が人々に恐れられる所以の一つともなっている点だろう。しかし、多重継承でも最初に継承されたものは、実体オブジェクトの先頭と同じポインタを持つ、だからそこの一点、つまり一番最初に継承された基底クラスであるということが、やけに重要味をもつことがある、というそれだけが言いたかったという話。しかももちろんコンパイラの実装次第で異なり得る話だ。これはあくまで手元のVC2008SP1で実験した結果をもとにした内容だ。
最後に、多重継承の場合のメモリ配置について、いろいろなありがたいリンクがある。
http://stackoverflow.com/questions/1002503/how-is-cs-multiple-inheritance-implemented
そこに掲げられている、

あたりが参考になりそうだ。
おいちょっとまて、上記の古いMSDN記事の中に、

An implementation is free to lay out the various embedded base instances and the new instance data in any order. Visual C++ is typical in laying out the base instances in declaration order, followed by the new data members, also in declaration order. (As we shall see, this is not necessarily the case when some bases have virtual functions and others don’t).

とあるから、最初に継承宣言されたからといって、その順番でメモリ配置がされるなんてことは、VCではあるかも知れないが、保障してるわけではないよ、ということか。
こういうinternal pointerの問題とかがあるから、C++とかで多重継承オブジェクトもサポートするようなガベージコレクションを作るのはあまり気が進まないよなあ。ガベコレ管理するなら多重継承は鬼門だなあ。多重継承の実装方法まで言語の仕様に入れることはできなかったのかなあ。