16

私は、MSDNドキュメントとECMA標準、およびVisual C ++Express2010を使用してC++/ CLIで遊んでいます。私を驚かせたのは、C++からの次の逸脱でした。

refクラスの場合、ファイナライザとデストラクタの両方を記述して、完全に構築されていないオブジェクトに対して複数回実行できるようにする必要があります。

私は少し例を作りました:

#include <iostream>

ref struct Foo
{
    Foo()  { std::wcout << L"Foo()\n"; }
    ~Foo() { std::wcout << L"~Foo()\n"; this->!Foo(); }
    !Foo() { std::wcout << L"!Foo()\n"; }
};

int main()
{
    Foo ^ r;

    {
        Foo x;
        r = %x;
    }              // #1

    delete r;      // #2
}

のブロックの終わりで#1、自動変数xが停止し、デストラクタが呼び出されます(通常のイディオムのように、ファイナライザが明示的に呼び出されます)。これはすべて問題ありません。しかし、それから私は参照を通してオブジェクトを再び削除しますr!出力は次のとおりです。

Foo()
~Foo()
!Foo()
~Foo()
!Foo()

質問:

  1. delete rオンラインで呼び出すのは未定義の動作ですか、それとも完全に許容でき#2ますか?

  2. 行を削除した場合、(C ++の意味で)もはや存在しないオブジェクトの追跡ハンドルである#2ことが重要ですか?rそれは「ぶら下がっているハンドル」ですか?その参照カウントは、二重削除が試みられることを意味しますか?

    出力が次のようになるため、実際の二重削除がないことを知っています。

    Foo()
    ~Foo()
    !Foo()
    

    しかし、それが幸せな事故なのか、明確な行動が保証されているのかはわかりません。

  3. 他のどのような状況で、管理対象オブジェクトのデストラクタを複数回呼び出すことができますか?

  4. x.~Foo();直前または直後に挿入しても大丈夫ですr = %x;か?

言い換えると、管理対象オブジェクトは「永久に存続」し、デストラクタとファイナライザの両方を何度も呼び出すことができますか?


自明でないクラスに対する@Hansの要求に応えて、このバージョンを検討することもできます(複数呼び出しの要件に準拠するように作成されたデストラクタとファイナライザを使用)。

ref struct Foo
{
    Foo()
    : p(new int[10])
    , a(gcnew cli::array<int>(10))
    {
        std::wcout << L"Foo()\n";
    }

    ~Foo()
    {
        delete a;
        a = nullptr;

        std::wcout << L"~Foo()\n";
        this->!Foo();
    }

    !Foo()
    {
        delete [] p;
        p = nullptr;

        std::wcout << L"!Foo()\n";
    }

private:
    int             * p;
    cli::array<int> ^ a;
};
4

2 に答える 2

18

私はあなたが提起する問題に順番に対処しようとします:

refクラスの場合、ファイナライザとデストラクタの両方を記述して、完全に構築されていないオブジェクトに対して複数回実行できるようにする必要があります。

デストラクタ~Foo()は、IDisposable :: Dispose()メソッドの実装と、ディスポーザブルパターンを実装する保護されたFoo :: Dispose(bool)メソッドの2つのメソッドを自動生成するだけです。これらは単純なメソッドであるため、複数回呼び出される可能性があります。C ++ / CLIでは、ファイナライザーを直接呼び出すことが許可されており、this->!Foo()通常は、同じように実行されます。ガベージコレクターはファイナライザーを1回だけ呼び出し、それが行われたかどうかを内部で追跡します。ファイナライザーを直接呼び出すことが許可されており、Dispose()を複数回呼び出すことが許可されている場合、ファイナライザーコードを複数回実行することができます。これはC++/ CLIに固有であり、他の管理対象言語では許可されていません。あなたはそれを簡単に防ぐことができます、nullptrチェックは通常仕事を終わらせます。

2行目でdeleterを呼び出すことは未定義の動作ですか、それとも完全に許容できますか?

それはUBではなく、完全に受け入れられます。deleteオペレーターは、IDisposable :: Dispose()メソッドを呼び出すだけで、デストラクタを実行します。その中で行うこと、非常に一般的にはアンマネージクラスのデストラクタを呼び出すことは、UBを呼び出す可能性があります。

行#2を削除した場合、rがまだ追跡ハンドルであることは重要ですか

いいえ。デストラクタの呼び出しは完全にオプションであり、強制するための適切な方法はありません。何も問題はありません。ファイナライザーは最終的に常に実行されます。与えられた例では、CLRがシャットダウンする前に最後にファイナライザースレッドを実行したときに発生します。唯一の副作用は、プログラムが「重く」実行され、必要以上にリソースを保持することです。

他のどのような状況で、管理対象オブジェクトのデストラクタを複数回呼び出すことができますか?

非常に一般的ですが、熱心なC#プログラマーは、Dispose()メソッドを複数回呼び出す可能性があります。CloseメソッドとDisposeメソッドの両方を提供するクラスは、フレームワークではかなり一般的です。別のクラスがオブジェクトの所有権を引き継ぐ場合など、ほぼ避けられないパターンがいくつかあります。標準的な例は、C#コードの次のビットです。

using (var fs = new FileStream(...))
using (var sw = new StreamWriter(fs)) {
    // Write file...
}

StreamWriterオブジェクトは、そのベースストリームの所有権を取得し、最後の中括弧でDispose()メソッドを呼び出します。FileStreamオブジェクトのusingステートメントは、Dispose()を2回呼び出します。これが起こらないように、そしてそれでも例外保証を提供するようにこのコードを書くことは非常に困難です。Dispose()を複数回呼び出すように指定すると、問題が解決します。

x。〜Foo();を挿入しても大丈夫でしょうか。r =%x;の直前または直後?

それは大丈夫。結果が快適である可能性は低く、NullReferenceExceptionが最も可能性の高い結果になります。これはテストする必要があるものであり、ObjectDisposedExceptionを発生させて、プログラマーにより良い診断を提供します。すべての標準の.NETFrameworkクラスはそうします。

言い換えれば、管理対象オブジェクトは「永遠に生きる」のでしょうか

いいえ、ガベージコレクターはオブジェクトが死んでいると宣言し、オブジェクトへの参照が見つからなくなったときにそれを収集します。これはメモリ管理のフェイルセーフな方法であり、削除されたオブジェクトを誤って参照する方法はありません。そのためには参照が必要なため、GCが常に参照します。循環参照のような一般的なメモリ管理の問題も問題ではありません。

コードスニペット

aオブジェクトを削除する必要はなく、効果はありません。IDisposableを実装するオブジェクトのみを削除し、配列は削除しません。一般的なルールは、.NETクラスは、メモリ以外のリソースを管理する場合にのみIDisposableを実装するというものです。または、それ自体がIDisposableを実装するクラスタイプのフィールドがある場合。

この場合、デストラクタを実装する必要があるかどうかはさらに疑問です。サンプルクラスは、かなり控えめな管理されていないリソースを保持しています。デストラクタを実装することにより、デストラクタを使用するためにクライアントコードに負担をかけることになります。クライアントプログラマーがそうするのがどれほど簡単かはクラスの使用法に強く依存します。オブジェクトがメソッドの本体を超えて長期間存続すると予想され、usingステートメントが使用できない場合は間違いありません。 。ガベージコレクターに、追跡できないメモリ消費量を通知するには、GC :: AddMemoryPressure()を呼び出します。これは、クライアントプログラマーがDispose()を使用しない場合も処理します。これは、ハードすぎるためです。

于 2012-09-09T15:56:52.390 に答える
1

標準のC++のガイドラインが引き続き適用されます。

  1. delete自動変数、またはすでにクリーンアップされている変数を呼び出すことは、依然として悪い考えです。

  2. これは、破棄されたオブジェクトへのトラッキングポインタです。そのような間接参照は悪い考えです。ガベージコレクションを使用すると、弱でない参照が存在する限りメモリが保持されるため、誤って間違ったオブジェクトにアクセスすることはできませんが、この破棄されたオブジェクトを有用な方法で使用することはできません。もう保持しません。

  3. 複数の破棄は、コードが標準のC ++ではUBであるような非常に悪いスタイルで記述されている場合にのみ、管理対象オブジェクトで発生する可能性があります(上記の1と下記の4を参照)。

  4. 自動変数でデストラクタを明示的に呼び出してから、自動破壊呼び出しが検出する場所に新しいデストラクタを作成しないことは、依然として悪い考えです。

一般に、オブジェクトの存続期間はメモリ割り当てとは別のものと考えます(標準のC ++と同じように)。ガベージコレクションは割り当て解除を管理するために使用されるため、メモリはまだ存在しますが、オブジェクトは無効です。標準のC++とは異なり、.NETランタイムの一部はメタデータがまだ有効であると想定する可能性があるため、そのメモリをrawバイトストレージに再利用することはできません。

ガベージコレクターも「スタックセマンティクス」(自動変数構文)も参照カウントを使用しません。

(醜い詳細:オブジェクトを破棄しても、そのオブジェクトに関する.NETランタイム自体の不変条件が壊れるわけではないので、おそらくそれをスレッドモニターとして使用することもできます。しかし、それは醜いデザインを理解するのが難しいので、しないでください。 't。)

于 2012-09-03T21:14:58.727 に答える