ぬうぱんの備忘録

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

作った曲一覧はこちら

多重継承とthisとnewとdeleteと(続き)

何があった

前の記事を公開したあと

というご指摘をいただきまして、確かになんだか動きそうな気がする! ということで実際に検証してみます。

試した環境

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

まずは動くのかどうか検証

なにはともあれ検証してみます。以下ソースコードとその実行結果。

#include <iostream>

using namespace std;

class B1{
private:
	int _Data;

public:
	B1()
	:_Data(0){
	}

	virtual ~B1(){
	}
};

class B2{
	int _Data;

public:
	B2()
	:_Data(0){
	}

	virtual ~B2(){
	}
};

class D : public B1, public B2{
private:
	int _Data;

public:
	D(): B1(), B2(), _Data(0){
	}

	virtual ~D(){
	}
};

int main(int argc, char *argv[]){
	D* pd = new D();
	B1* pb1 = pd;
	B2* pb2 = pd;
	cerr << "pd : " << pd << endl;
	cerr << "pb1 : " << pb1 << endl;
	cerr << "pb2 : " << pb2 << endl;
	delete pb2;
	return 0;
}
pd : 0x2165010
pb1 : 0x2165010
pb2 : 0x2165020

エラーも出ずにちゃんと動いた。実行結果を見るとdeleteには+0x10されたアドレスが渡されてるように見える。なのに問題なくdeleteされているということは何処かの段階でアドレスが本来の先頭アドレスに直されているということだろう。はてどこで?

new/deleteを多重定義してみる

new/deleteを多重定義してdeleteに渡ってくるアドレスを見てみる。もし、渡されたアドレスが修正済みのアドレスならdeleteの呼び出しをコンパイラが上手いこと(dynamic_cast使うとか)してくれているということになる。ということでクラスDを以下のように修正。クラスB1とクラスB2もおんなじように修正。main関数の方は特に変更なし。

class D : public B1, public B2{
private:
	int _Data;

public:
	D(): B1(), B2(), _Data(0){
	}

	virtual ~D(){
	}

	static void* operator new(size_t size){
		cerr << "D::operator new has called." << endl;
		return malloc(size);
	}

	static void operator delete(void* p){
		cerr << "D::operator delete has called : " << p << endl;
		free(p);
	}
};

実行結果は以下の通り

D::operator new has called.
pd : 0x1de2010
pb1 : 0x1de2010
pb2 : 0x1de2020
D::operator delete has called : 0x1de2010

クラスDのdeleteが呼び出されているし、アドレスも本来の先頭アドレスになっている。ダメ押しで各クラスの仮想デストラクタをコメントアウトして(B1,B2,Dをポリモーフィックな型じゃなくして)もう一度実行してみる。

D::operator new has called.
pd : 0x14ff010
pb1 : 0x14ff010
pb2 : 0x14ff014
B2::operator delete has called : 0x14ff014
*** glibc detected *** ./xxxxxxxxxxx: free(): invalid pointer: 0x00000000014ff014 ***
.
.
.

アウト。B2のdeleteが呼ばれてアドレスもB2の先頭アドレスのままになっている。

結局どういうことなのか?

規格書(JIS X3014)を見てみるとこう書いてあった(N3242もチェックしてみたが同じことが書いてあった)

5.3.5 delete式 delete式は<>が作った最派生オブジェクト(1.8)又は配列を解体する。
(p.66)

3 第1形式(オブジェクト用)において,演算対象の静的な型がその動的な型と異なる場合,その静的な型は,演算対象の動的な型の基底クラスでなければならず,仮想デストラクタを持っていなければならない。そうでない場合の動作は未定義とする。第2形式(配列用)において,オブジェクトの動的な型がその静的な型と異なる場合の動作は,未定義とする。(73)
(p.67)

どうやら基底クラスのポインタをdeleteに渡しても本来の型として扱ってくれるみたいだ。ただし、deleteで指定した型に仮想デストラクタが存在していることを前提としていて、存在しない場合どうなるかはコンパイラ次第。仮想デストラクタなしでpb1をdeleteしても何も問題は起きなかったので、gccではそのままのアドレスをdeleteに渡すようだ。
なお、必要なのは仮想デストラクタであって、仮想関数作ったから仮想関数テーブルあるしオッケーという話ではないので注意(実際、この状態だと警告が出る。警告出せるんならエラーで止めてほしい気もする。)。
それから、多重継承でない普通の継承の場合、仮想デストラクタなんかなくてもちゃんと動きますが、これはコンパイラ様がうまくやってくれているだけの話で、実際は未定義の動作なのでどっちにしろ仮想デストラクタは書くべきです。
仮想デストラクタを持ってないと派生クラスのデストラクタが呼ばれないとかいう話は聞いたことがアリますが、deleteにおいても重要なんすね・・・。

最派生オブジェクト is 何

さて、仕様書には最派生オブジェクトという言葉が出てきたが、これがよくわからない。規格書には

4 クラス型の,総体オブジェクト,データメンバ(9.2)又は配列要素の型としては,基底クラス部分オブジェクトのクラス型と区別するため,その最派生クラス(most derived class)を採択する。最派生クラス型のオブジェクトを最派生オブジェクト(most derived object)と呼ぶ。
(p.4)

としか書いてない。単に探し方が悪いだけで、どっかに書いてるんですかね? ともかく、よくわからないので、以下のクラスを追加してもっかいコードを実行してみる。

class D1 : D{
private:
	int _Data;

public:
	D1()
	:D(){
	}

	~D1(){
	}
};
D::operator new has called.
pd : 0xaa3010
pb1 : 0xaa3010
pb2 : 0xaa3020
D::operator delete has called : 0xaa3010

クラスDを継承したクラスD1を定義して同じコードを実行しても結果は変わらなかった。クラスD1の定義は存在するものの、クラスDとしてnewされたオブジェクトは他に含まれていない総体オブジェクトなので、このケースにおける最派生クラスはクラスDっていうことでしょうか? 教えてエロイ人

キャストによるあれこれ

deleteに部分オブジェクトのポインタを渡しても適切に取り扱ってくれることはわかった。けど、途中でdynamic_cast以外のキャストを使ってvoid*にキャストしたら・・・? ということでmain()を以下のように書きかえて検証。

int main(int argc, char *argv[]){
	D* pd = new D();
	B2* pb2 = pd;
	void* pvb2 = pb2;
	cerr << "unexplicit cast : " << pb2 << endl;
	cerr << "static cast : " << static_cast<void*>(pb2) << endl;
	cerr << "dynamic cast : " << dynamic_cast<void*>(pb2) << endl;
	cerr << "reinterpret cast : " << reinterpret_cast<void*>(pb2) << endl;
	delete pb2;
	return 0;
}

実行結果は以下の通り

D::operator new has called.
unexplicit cast : 0x135a020
static cast : 0x135a020
dynamic cast : 0x135a010
reinterpret cast : 0x135a020
D::operator delete has called : 0x135a010

dynamic_cast以外はRTTIを考えずにそのままのアドレスになっている。当然といえば当然でしょうかね。アタリマエのことですが``delete static_cast(pb2);''はアウトです。

純粋仮想デストラクタは?

仮想デストラクタがないとダメらしいけど、じゃあ純粋仮想デストラクタは? って思って試しにB2::~B2()を純粋仮想デストラクタにしてみたら問題ありませんでした。

結局

いろいろ検証しましたが、結局は

    • 継承関係があってnew/deleteされる可能性のあるクラスには(純粋)仮想デストラクタを書いておけ
    • 継承関係の存在するクラスへのポインタをvoid*にキャストする時は本当にしていいのかよく考えろ

ってところでしょうか。仮想デストラクタについては

    • 継承関係のあるクラスは特別な理由がない限り仮想デストラクタは書いておけ

くらいに思っておけばOKですかね?

感想

 いろいろおべんきょうになりました