デストラクタを仮想関数にしたとき動き(2)

解決


wave  2008-06-11 19:28:20  No: 68515

MSVS2008で以下のソースを実行すると落ちます。MSDEV6.0でも同じです。
理由がわからず困っています。よろしくお願いします。

class A {
public:
        A();
        ~A();
};
A::A() { cout << "A()"; }
A::~A() { cout << "~A()"; }

class B : public A {
public:
        B();
        virtual ~B();
};
B::B() { cout << "B()" ; }
B::~B() { cout << "~B()" ; }

void Test() {
        A* b = new(B);
        delete b;
}

---------------------
Debug Assertain Faild!
...
File: f:\dd\vctools\crt_bld\self_x86\crt\src\dgbdel.cpp
Line: 52
...
Expression: _BLOCK_TYPE_IS_VALID(pHeap->nBlockUse)
...

ヒープが壊れていることが原因として考えられます


nowhere  2008-06-11 20:11:22  No: 68516

virtualをつけない場合、関数をオーバーライドしても現在の型の関数が呼び出されます。
具体的に言うと、上記の例では delete b; をしたときに、A::~A() は仮想関数ではないので、(本当の実体がBなのに)A::~A()が呼び出されます。
B::~B()が呼び出さず、結果的にBの分のメモリーが開放されず、上記のようなエラーが通知されるわけです。

A::~A()をvirtualで定義すれば、エラーはでなくなると思います。

C++でクラスの継承を行う場合、必ずデストラクタはvirtualで宣言するのは鉄則です。

http://www.gcc.ne.jp/~narita/prog/advanced/03/index.html
などを参照してください。


επιστημη  URL  2008-06-11 20:18:29  No: 68517

> 結果的にBの分のメモリーが開放されず、上記のようなエラーが通知されるわけです。

~B() のvirutalを取っ払うとAssertionで引っかからなくなるんですよ。
それを説明できているでしょうか。

もちろん、BをnewしたのにAとしてdeleteするんだからヘンなことが起こるのは当然です。~B()がvirtualか否かを問わず。
そのヘンなことが"具体的になにか"は答えられません。処理系によりけりでしょう。


wave  2008-06-12 00:12:57  No: 68518

nowhereさん、ありがとうございます。

メモリリークではなく、不正な処理を行おうとして、アサートされているようなんです。

> C++でクラスの継承を行う場合、必ずデストラクタはvirtualで宣言するのは鉄則です。

そういう原則があるのは承知しています。
この原則を守らず、サブクラスで動的にメモリ確保している場合には、
メモリリークが起きるのは当然だと思います。

ただパフォーマンス等の考慮であえて仮想化しないこともあるでしょうし、
何より、そうしないからといって落ちるのも当然なんでしょうか?

επιστημηさん、ありがとうございます。

> ~B() のvirutalを取っ払うとAssertionで引っかからなくなるんですよ。

そうなんです、これがわからないことなんです。

> BをnewしたのにAとしてdeleteするんだからヘンなことが起こるのは当然です。

これ自体は普通の操作ですし、これだけでヘンなこと起きると困りますよ。
~A() が仮想関数なら ~B()~A() が呼ばれ、そうでなければ ~A() のみが呼ばれるのを期待します。


tetrapod  2008-06-12 00:31:53  No: 68519

> これ自体は普通の操作ですし、これだけでヘンなこと起きると困りますよ。
ヘンな操作、言語規格書で禁じられている誤ったコードであるですよ
ISO/IEC 14882:1998 5.3.5 - 3 前半

> ~A() が仮想関数なら ~B()~A() が呼ばれ、
ここまでは言語規格書が保証している正しいコードであって

> そうでなければ ~A() のみが呼ばれるのを期待します。
ここは言語規格書が未定義としている誤ったコードとなる

Release モードだと誤りが検出されないのに対して
Debug モードだと親切にも誤りを検出してくれるので感謝しなきゃ


wave  2008-06-12 03:29:57  No: 68520

tetrapod さんありがとうございます。

確かにベースクラスのデストラクタを仮想関数として宣言しない場合の動作は「未定義」だと書かれています。
C++の標準規格では「未定義」だということはよくわかりました。よくぞこういう記述をご提示くださいました。

私が見た本やネットでの解説では、ベースクラスでコンストラクタを仮想関数として宣言する理由は
「サブクラスのデストラクタがコールされるようにする為」としか書かれていませんでした。
ですから貴重な収穫です。

ただ、

> ここは言語規格書が未定義としている誤ったコードとなる

「未定義」=「誤ったコード」ではなく、仕様が未定義なだけですから、実装に依存するということだと思います。
C++標準規格ではなく、MSの仕様としてどうなっているのかを確認する必要があるようですね。
特に今回はサブクラスで仮想関数としてl宣言した場合だけ落ちるわけですからね。

> Release モードだと誤りが検出されないのに対して
> Debug モードだと親切にも誤りを検出してくれるので感謝しなきゃ

これはコードの誤りを検出してるのではなく、メモリの不正アクセスを検出してるだけなので;

------------------------
5.3.5 - Delete
...
... if the static type of the operand is different from its dynamic type, the static type shall be a base class of the operand's dynamic type and the static type shall have a virtual destructor or the behavior is undefined. ...
...

...演算対象の静的な型がその動的な型と異なる場合、その静的な型は、演算対象の動的な型の根底クラスでなければならず、仮想デストラクタを持っていなければならない。そうでない場合の動作は未定義とする。...


wave  2008-06-12 03:31:44  No: 68521

すみません訂正です。

×:私が見た本やネットでの解説では、ベースクラスでコンストラクタを仮想関数として宣言する理由は
○:私が見た本やネットでの解説では、ベースクラスでデストラクタを仮想関数として宣言する理由は


tetrapod  2008-06-12 04:00:08  No: 68522

仕様書の「未定義」という言葉は「仕様書が定めていない」ではなくてきっちり決まっている。
1.3.12 未定義の動作(undefined-behavior)
間違ったプログラム断片または間違ったデータに対して生じうる動作
例・その状況を完全に無視して何が起こるかわからない
例・適宜動作する(その処理系作者は文書を用意し説明すること)
例・翻訳または実行を終了する
ということで VC++ の動作は規格書に完全合致だ。

ここでいう「何が起こるかわからない」というのは
・プログラマの勝手な期待通りに動作する
・予測不能な動作を行う(いわゆる鼻から悪魔って奴だ)
ということで。

そのコード、 VC++ 以外では絶対に使用しないということ?
VC++での動きと他の処理系での動きは異なる可能性が大なわけで、言語規格書にて未定義であっても
・VC++でならうまく(=プログラマの勝手な期待通りに)動く
・***社の###コンパイラでは(期待通りに)動かない
ということであってもかまわない、ということで?


wave  2008-06-12 06:06:33  No: 68523

tetrapodさん、重ねてのご回答ありがとうござます。

MSVC++ が ISO/IEC 14882 から外れていなことは、先のご回答ではっきり承知しています。

で、今は

> 例・適宜動作する(その処理系作者は文書を用意し説明すること)

となっている気がして、、、
その仕様書にあたる物があったらいいなぁと思っているわけです。
その仕様書にサブクラスでデストラクタを仮想関数とした場合の挙動の説明があれば一番嬉しいわけです。

或いは「ベースクラスのデストラクタは必ず仮想関数とする必要があります、そうしないと落ちます」
と書かれていてもいいです(ないでしょうけど)。
その仕様をどう設定しようとMSの自由だと思います。ただ、どうなっているかが知りたいんです。

MSのマニュアルとか探してみますね。

> そのコード、 VC++ 以外では絶対に使用しないということ?

そういう前提で構いません。今 MSVS2008 や MSDev6.0 で落ちる理由が知りたいのですから。

これからソース書くときの話ではなく(それならそんなコード書くな!で終わりでしょうけど)、
実在してるソースが落ちる原因を究明したいのです。

というか MSVS2008 が ISO/IEC 14882 に完全準拠なのかっていう話も。。。


tetrapod  2008-06-12 06:45:58  No: 68524

うむ。ぜひ探してください。で、見つけたらここで教えてくんさい。
賭けるとしたら俺は、そんな対外文書は見つからないに100ぺリカ
> 例・翻訳または実行を終了する
であるという文書が見つかるに5ぺリカ

> MSVS2008 や MSDev6.0 で落ちる理由が知りたいのですから。
理由を知ってどうするのか非常に微妙。単に自己満足したいってこと?
だいたいReleaseモードでは落ちないんだけど。
# 正確には不正が見つからないだけなんだけど。

俺としては「バグっているコードがどう動こうが所詮バグってるんだからどーでもいい」と思う。
正しく修正するか、自分の責任で動作保障するか、どっちかしかないぢゃん。

で、以下は技術論なんだけど
「デストラクタが virtual でないクラスは、そのクラスの作者が派生禁止という意思表示をしている」
ということだと解釈しなきゃならない (Effective C++)
派生禁止であると意思表明しているクラスから強引に派生クラスを作ることはできる
ただし、そんなことしても動作保証がない。ということで。

こういう過去ログを発掘 (って俺ぢゃん)
http://rararahp.cool.ne.jp/cgi-bin/lng/vc/vclng.cgi?print+200404/04040043.txt


yoh2  2008-06-12 08:30:30  No: 68525

> MSVS2008 や MSDev6.0 で落ちる理由が知りたいのですから。
単なる知的好奇心で知りたいだけかもしれないので、多分以下が理由じゃないかな、
と言ってみます。

1. Aには一切virtualな関数がない場合、vtblを先頭に持ってくるために
   Bのインスタンスは以下の様な構造になる。

Bのインスタンス
 +- vtbl
 +- 部分オブジェクト(A)
 +- Bの残りメンバ

2. この結果、部分オブジェクトAのアドレスはoperator new()で得られたアドレス
   ではなくなる。

3. A*型のままdeleteすると、2でずれたアドレスをoperator delete()に渡して
   しまい、メモリ管理機構が混乱する。

ただし、仕事等で理由を説明しなければならないとしたら、下手に落ちる理由を説明
するよりも、
  - 問題のコードは間違っているコード。何が起こっても不思議ではない。
  - そして今回は「たまたま」落ちるという結果になっただけ。
    その理由を詮索するのは無意味。
という点をきちんと説明する方がかえって正しい姿勢だと思います。
(もしこの理由について説明している文書がなければ。 --「未定義」の場合、
そのような文書を作る義務がない(そして大抵作られていない)ことに注意)

今回とは違うケースですが、未定義の挙動についての質問と回答がC FAQに
あります。参考までに。
http://www.kouno.jp/home/c_faq/c3.html#3
http://www.kouno.jp/home/c_faq/c11.html#33


Ban  2008-06-12 17:25:54  No: 68526

> その仕様をどう設定しようとMSの自由だと思います。
> ただ、どうなっているかが知りたいんです。
> MSのマニュアルとか探してみますね。

コンパイラ屋さんも、将来の変更に際して互換性には配慮するわけで、
規格上undefined behaviorであっても、自分のとこで明確に文書化したものは、
極力維持しようという制約が出てくるのではないかと思われます。
# まぁ元がundefined behaviorですし、さくっと挙動を変わっても怒る気はありませんが、
# 世の万人がみんなそんな物分りとは思えませんし…。

文書を要求するimplementation-definedとは違って、
undefined behaviorの実際の動作は確かに処理系に依存するものの
文書化して保証する義務も義理もないですし、
たいてい探しても文書はないと思いますけれど…

経験上、ReleaseとDebugの挙動に大きな差異がある場合(今回みたいなのを含め)、
たいていはundefined behaviorの範囲に落ち込んでいて、
挙動が違っておかしくない部分で挙動が違う。文書など当然ないことが多いです。

> そういう前提で構いません。今 MSVS2008 や MSDev6.0 で落ちる理由が知りたいのですから。

技術的な話でいえば、yoh2さんの書かれた説明はあり得ると思います。
本当にそうか調べてみたいなら当該処理をアセンブリで追ってみればいいのでは。

動作保証上の話で言えば「言語仕様として保証のないコードを書いた結果、落ちる」で
概ね終了だと思いますし。


tetrapod  2008-06-12 17:47:54  No: 68527

どこが「メモリ破壊」しているか?と上司に問われて困っている、のであれば以下のコードなどいかが?
char* p=new char;
delete (p+1);
どこも「メモリ破壊」していないのに同じダイアログが表示されるよ


仲澤@失業者  2008-06-12 19:42:28  No: 68528

要するに
「デストラクタとメンバ関数を比べると、virtualに対する動作が
違うので気に入りません」ということのように読めます。

コンストラクタ、デストラクタは生成破棄自体を行うもので、
オブジェクトの生成後にしか機能しないメンバ関数とは異なる動作
をするのは当たり前。
としか言いようがありません。


wave  2008-06-12 23:34:26  No: 68529

tetrapod さん、ありがとうございます。

> 理由を知ってどうするのか非常に微妙。単に自己満足したいってこと?

その理由によって、修正したり、しなかったり。自己満足で終わるかも知れません。

> 「デストラクタが virtual でないクラスは、そのクラスの作者が派生禁止という意思表示をし

ている」
> ということだと解釈しなきゃならない (Effective C++)
> 派生禁止であると意思表明しているクラスから強引に派生クラスを作ることはできる
> ただし、そんなことしても動作保証がない。ということで。

「Effective C++ で禁止されている」=「動作保証がない」とは思ってないです。
もちろん新しく書くソースで準拠するのは何の問題もありませんが、
既に存在してるをソースをこれを理由に治すのは躊躇しますね。
他人のソースだし、私自身この本読んだの97年だし、それ以前にもソースは存在してるし。。

yoh2さん、ありがとうございます。

おかげで、すっきりしました。

1.2.3. の流れで、確かにBに仮想関数がないときはずれません。
デストラクタに限らずBに仮想関数があるとずれます。
そしてBに仮想関数があってもAに仮想関数があればずれません。

なるほど、、、ようするに、キャスト時の問題ですね。
        A* b = new(B);
        void* p1 = b;
        void* p2 = (B*)b;
ここで p1 == p2 の場合と p1 =! p2 になる場合があるのは当然で、
その差異から生じる問題ですね。極ありふれた問題でした。

> その理由を詮索するのは無意味。

これ以上詮索する必要はありませんが、ただ私の中ではここまで至ってなかったもので。。
この時点で「解決」とさせて頂きます。

Banさん、ありがとうございます。

> 動作保証上の話で言えば「言語仕様として保証のないコードを書いた結果、落ちる」で
概ね終了だと思いますし。

そうなんでしょうが、終了しなくてよかったです。

仲澤さん、ありがとうございます。
そういう意図はありませんでした。文章が下手ですみません。

結局はεπιστημηさんが最初にご回答して下さったことに他ならないわけで、
理解力が低くて申し訳ないです。他の人にも余計な回答をさせてしまって。。。

皆様ありがとうございました。


wave  2008-06-12 23:37:31  No: 68530

すみません、解決チェックの追加です。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加