読者です 読者をやめる 読者になる 読者になる

ぬうぱんの備忘録

技術系のメモとかいろいろ

作った曲一覧はこちら

多重継承とthisとnewとdeleteと

C++ Tips トラブルシューティング

何があった

多重継承のケースで

    • コンストラクタの初期化子リスト内でthisはどうなっているのか?
    • 初期化子リストにthisを渡すと何がまずいのか?
    • 多重継承でnewでもらったポインタとdeleteに渡すポインタが一致しない時どうなってしまうのか?

多重ではない通常の木構造の継承の時の話は何も問題ないので触れないです。

追記(2014/6/5)

多重継承とdeleteのお話についてはちゃんとあれこれ調べたのでそちらを参照してください

試した環境(追記:2014/6/5)

g++-4.7.2(Ubuntu/Linaro 4.7.2.2-2ubuntu1)

コンストラクタの初期化子リスト内でのthisポインタ

多重継承のクラス作って初期化子リスト内でアドレスをダンプしてみる。

#include <iostream>

using namespace std;

class CPrint;
class B1;
class B2;
class D;

class CPrint{
public:
	CPrint(void* p, const std::string& label){
		cerr << label << " : " << p << endl;
	}
};

class B1{
private:
	int _Data;
	CPrint _Print1;
	CPrint _Print2;

public:
	B1(D* parent)
	:_Data(0), _Print1(this, "B1"), _Print2(parent, "B1_parent"){
	}
};

class B2{
	int _Data;
	CPrint _Print;

public:
	B2()
	:_Data(0), _Print(this, "B2"){
	}
};

/*!
 */
class D : public B1, public B2{
private:
	int _Data;
	CPrint _Print;

public:
	D(): B1(this), B2(), _Data(0), _Print(this, "D"){
	}
};

int main(int argc, char *argv[]){
	D* pd = new D();
	B1* pb1 = pd;
	B2* pb2 = pd;

	CPrint(pd, "pd");
	CPrint(pb1, "pb1");
	CPrint(pb2, "pb2");

	delete pd;

	return 0;
}
B1 : 0x603010
B1_parent : 0x603010
B2 : 0x603018
D : 0x603010
pd : 0x603010
pb1 : 0x603010
pb2 : 0x603018

特に問題無し。
初期化子リスト中にダンプしたアドレスと構築終了後にダンプしたアドレスは間違いなく一致している。

VC++だと初期化子リスト中でthisを渡すとC4355が出るけど何がまずいいのか?

相対アドレスが・・・とかいう話だと何故か思い込んでたけど、初期化が完全に終了していないインスタンスへのポインタを第三者に渡してしまうのが問題。
だから例えばこんなコードを書くと実行時にエラーになる。

class D;

class B{
private:
	int _Data;
public:
	B(D* parent);
};

class D : public B{
public:
	D():B(this){
	}

	virtual int hoge(){
		return 0;
	}
};

B::B(D* parent)
:_Data(parent->hoge()){	//<-未初期化の仮想関数テーブルを引いてアウト!
}

ただ、アドレスの計算が間違っているとかいう話ではなく(前述)、参照した先が未初期化であることが問題。ということは、thisを渡した先(クラスB)で自分自身(クラスD)が弄られないことが保証されるのなら、初期化子リスト内でthisを使っちゃっても大丈夫。
なお、コンパイラ様はこのコードが問題を引き起こすと断定できない(D* pが未初期化のDへのポインタであるとは限らない)ので止めてくれない。

多重継承でnew/delete

最初のコードのmain()関数を以下のように置き換えて、newでもらったアドレスとdeleteに渡すアドレスがずれた時の動作を見る。

int main(int argc, char *argv[]){
	D* pd = new D();
	B2* pb2 = pd;
	CPrint(pd, "pd");
	CPrint(pb2, "pb2");
	delete pb2; //<-ここでエラー!
	return 0;
}

実行結果は

B1 : 0x198c010
B1_parent : 0x198c010
B2 : 0x198c018
D : 0x198c010
pd : 0x198c010
pb1 : 0x198c010
pb2 : 0x198c018
*** glibc detected *** ./make_distortion_image: free(): invalid pointer: 0x000000000198c018 ***

当然ながら(仮想デストラクタが存在しない未定義のコードなので(2014/6/5:追記))エラー。これの何が問題かって、newした派生クラスを相対アドレスの違う基底クラスのポインタに変換してdeleteするともれなくお陀仏ということ。この、(多重継承に限らず)基底クラスのポインタとしてdeleteというのは十分ありえるというか普通はこうなるのでとっても厄介。なお、B1*としてdeleteした場合は(今回のケースに限って言えば)アドレスが一致しているので何か対処をしなくてもエラーにならない。
対処法としてはdynamic_castでvoid*にして(RTTIの力で本来の先頭アドレスを得る)deleteするという手がある。今回の例ならB1, B2, Dに仮想デストラクタを追加してmainを以下のように書き換える。

int main(int argc, char *argv[]){
	D* pd = new D();
	B1* pb1 = pd;
	B2* pb2 = pd;
	void* pvb2 = dynamic_cast<void*>(pd);
	CPrint(pd, "pd");
	CPrint(pb1, "pb1");
	CPrint(pb2, "pb2");
	CPrint(pvb2, "pvb2");
	delete pvb2;
	return 0;
}
B1 : 0x603010
B1_parent : 0x603010
B2 : 0x603020
D : 0x603010
pd : 0x603010
pb1 : 0x603010
pb2 : 0x603020
pvb2 : 0x603010

確かに正しい先頭アドレスをとれている。
わざわざdynamic_castしなくても仮想デストラクタを追加して``delete pb2;''すれば問題ない(2014/6/5:追記)

結論

    • 初期化リスト内でthisを渡す時はその先で(少なくとも自分のコンストラクトが完了するまで)自分自身を弄られないことを保証しろ
    • 派生クラスをnewして多重継承の基底クラスのポインタでdeleteする時はdynamic_castの使用を検討しろ続きの記事を見てね♡(2014/6/4:追記)

感想

 多重継承コワイ!

蛇足

 google-perftoolsのtcmallocリンクしてたらアドレスがずれてる状態でdeleteしても死ななかった。コワイ。