51

このプログラムは明確に定義されていますか?そうでない場合は、なぜ正確ですか?

#include <iostream>
#include <new>
struct X {
    int cnt;
    X (int i) : cnt(i) {}
    ~X() {  
            std::cout << "destructor called, cnt=" << cnt << std::endl;
            if ( cnt-- > 0 )
                this->X::~X(); // explicit recursive call to dtor
    }
};
int main()
{   
    char* buf = new char[sizeof(X)];
    X* p = new(buf) X(7);
    p->X::~X();  // explicit call to dtor
    delete[] buf;
}

私の推論:12.4 / 14によると、デストラクタを2回呼び出すことは未定義の動作ですが、正確には次のようになります。

存続期間が終了したオブジェクトに対してデストラクタが呼び出された場合、動作は未定義です。

これは再帰呼び出しを禁止していないようです。オブジェクトのデストラクタが実行されている間、オブジェクトの存続期間はまだ終了していないため、デストラクタを再度呼び出すのはUBではありません。一方、12.4/6は次のように述べています。

本体を実行した後[...]クラスXのデストラクタは、Xの直接メンバーのデストラクタ、Xの直接基本クラスのデストラクタを呼び出します[...]

つまり、デストラクタの再帰呼び出しから戻った後、すべてのメンバーおよび基本クラスのデストラクタが呼び出され、前のレベルの再帰に戻ったときに再度呼び出すとUBになります。したがって、ベースがなく、PODメンバーのみが存在するクラスは、UBなしで再帰デストラクタを持つことができます。私は正しいですか?

4

5 に答える 5

60

§3.8/1の「生涯」の定義のため、答えはノーです。

タイプのオブジェクトの存続期間は、次の場合にT終了します。

Tが自明でないデストラクタ(12.4)を持つクラスタイプの場合、デストラクタ呼び出しが開始されるか、または

—オブジェクトが占有するストレージが再利用または解放されます。

デストラクタが(最初に)呼び出されるとすぐに、オブジェクトの存続期間が終了します。したがって、デストラクタ内からオブジェクトのデストラクタを呼び出す場合、§12.4/ 6に従って、動作は定義されていません。

存続期間が終了したオブジェクトに対してデストラクタが呼び出された場合、動作は未定義です。

于 2010-06-17T16:01:50.443 に答える
9

さて、動作が定義されていないことを理解しました。しかし、実際に起こったことへの小さな旅をしましょう。私はVS2008を使用しています。

これが私のコードです:

class Test
{
int i;

public:
    Test() : i(3) { }

    ~Test()
    {
        if (!i)
            return;     
        printf("%d", i);
        i--;
        Test::~Test();
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    delete new Test();
    return 0;
}

それを実行して、デストラクタ内にブレークポイントを設定し、再帰の奇跡を起こさせましょう。

スタックトレースは次のとおりです。

代替テキスト

それは何scalar deleting destructorですか?これは、コンパイラがdeleteと実際のコードの間に挿入するものです。デストラクタ自体は単なるメソッドであり、特別なことは何もありません。それは実際にはメモリを解放しません。その中のどこかでリリースされscalar deleting destructorます。

scalar deleting destructorに行って、分解を見てみましょう:

01341580  mov         dword ptr [ebp-8],ecx 
01341583  mov         ecx,dword ptr [this] 
01341586  call        Test::~Test (134105Fh) 
0134158B  mov         eax,dword ptr [ebp+8] 
0134158E  and         eax,1 
01341591  je          Test::`scalar deleting destructor'+3Fh (134159Fh) 
01341593  mov         eax,dword ptr [this] 
01341596  push        eax  
01341597  call        operator delete (1341096h) 
0134159C  add         esp,4 

再帰を実行している間、アドレス01341586でスタックし、メモリは実際にはアドレスでのみ解放されます01341597

結論:VS 2008では、デストラクタは単なるメソッドであり、すべてのメモリ解放コードが中間関数()に挿入されるscalar deleting destructorため、デストラクタを再帰的に呼び出すのは安全です。しかし、それでもそれは良い考えではありません、IMO。

編集:わかりました、わかりました。この答えの唯一のアイデアは、デストラクタを再帰的に呼び出すときに何が起こっているかを調べることでした。しかし、それをしないでください、それは一般的に安全ではありません。

于 2010-06-17T16:28:01.093 に答える
5

これは、オブジェクトの存続期間に関するコンパイラの定義に戻ります。のように、メモリが実際に割り当て解除されるのはいつですか。デストラクタはオブジェクトのデータにアクセスできるため、デストラクタが完了するまでは不可能だと思います。したがって、デストラクタへの再帰呼び出しが機能することを期待します。

しかし...デストラクタとメモリの解放を実装する方法は確かにたくさんあります。今日使用しているコンパイラで期待どおりに機能したとしても、そのような動作に依存することには非常に注意が必要です。ドキュメントに機能しない、または結果が予測できないと書かれていることがたくさんありますが、実際に内部で何が起こっているのかを理解していれば、実際には問題なく機能します。しかし、本当に必要な場合を除いて、それらに依存することは悪い習慣です。仕様でこれが機能しないと記載されている場合、実際に機能する場合でも、次のバージョンで機能し続けるという保証はありません。コンパイラ。

とはいえ、本当にデストラクタを再帰的に呼び出したい場合、これが単なる架空の質問ではない場合は、デストラクタの本体全体を別の関数にリッピングし、デストラクタにそれを呼び出しさせてから、その呼び出し自体を再帰的にさせてはどうでしょうか。それは安全なはずです。

于 2010-06-17T17:06:09.067 に答える
1

ええ、それは正しいように聞こえます。デストラクタの呼び出しが終了すると、メモリは割り当て可能なプールにダンプされ、何かが上書きできるようになるため、フォローアップのデストラクタ呼び出しで問題が発生する可能性があります(「this」ポインタは無効になります)。

ただし、再帰ループが解かれるまでデストラクタが終了しない場合は、理論的には問題ありません。

興味深い質問:)

于 2010-06-17T16:00:32.233 に答える
0

なぜ誰もがこのようにデストラクタを再帰的に呼び出したいと思うのでしょうか?デストラクタを呼び出すと、オブジェクトが破棄されます。もう一度呼び出すと、実際に同時に破壊している途中で、すでに部分的に破壊されたオブジェクトの破壊を開始しようとしていることになります。

すべての例には、呼び出しで本質的にカウントダウンするための、ある種のデクリメンタル/インクリメンタル終了条件があります。これは、それ自体と同じタイプのメンバーを含むネストされたクラスのある種の失敗した実装を示唆しています。

このようなネストされたマトリョーシカクラスの場合、メンバーでデストラクタを再帰的に呼び出します。つまり、デストラクタはメンバーAでデストラクタを呼び出し、次にメンバーAでデストラクタを呼び出し、次にデストラクタを呼び出します。完全に正常で、期待どおりに機能します。これはデストラクタの再帰的な使用ですが、それ自体でデストラクタを再帰的に呼び出すことはなく、ほとんど意味がありません。

于 2017-08-05T23:57:16.243 に答える