Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
268 views
in Technique[技术] by (71.8m points)

garbage collection - Repeated destructor calls and tracking handles in C++/CLI

I'm playing around with C++/CLI, using the MSDN documentation and the ECMA standard, and Visual C++ Express 2010. What struck me was the following departure from C++:

For ref classes, both the finalizer and destructor must be written so they can be executed multiple times and on objects that have not been fully constructed.

I concocted a little example:

#include <iostream>

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

int main()
{
    Foo ^ r;

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

    delete r;      // #2
}

At the end of the block at #1, the automatic variable xdies, and the destructor is called (which in turn calls the finalizer explicitly, as is the usual idiom). This is all fine and well. But then I delete the object again through the reference r! The output is this:

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

Questions:

  1. Is it undefined behavior, or is it entirely acceptable, to call delete r on line #2?

  2. If we remove line #2, does it matter that r is still a tracking handle for an object that (in the sense of C++) no longer exists? Is it a "dangling handle"? Does its reference counting entail that there will be an attempted double deletion?

    I know that there isn't an actual double deletion, as the output becomes this:

    Foo()
    ~Foo()
    !Foo()
    

    However, I'm not sure whether that's a happy accident or guaranteed to be well-defined behaviour.

  3. Under which other circumstances can the destructor of a managed object be called more than once?

  4. Would it be OK to insert x.~Foo(); immediately before or after r = %x;?

In other words, do managed objects "live forever" and can have both their destructors and their finalizers called over and over again?


In response to @Hans's demand for a non-trivial class, you may also consider this version (with destructor and finalizer made to conform to the multiple-call requirement):

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

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

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

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

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

private:
    int             * p;
    cli::array<int> ^ a;
};
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I'll just try to address the issues you bring up in order:

For ref classes, both the finalizer and destructor must be written so they can be executed multiple times and on objects that have not been fully constructed.

The destructor ~Foo() simply auto-generates two methods, an implementation of the IDisposable::Dispose() method as well as a protected Foo::Dispose(bool) method which implements the disposable pattern. These are plain methods and therefore may be invoked multiple times. It is permitted in C++/CLI to call the finalizer directly, this->!Foo() and is commonly done, just like you did. The garbage collector only ever calls the finalizer once, it keeps track internally whether or not that was done. Given that calling the finalizer directly is permitted and that calling Dispose() multiple times is allowed, it is thus possible to run the finalizer code more than once. This is specific to C++/CLI, other managed languages don't allow it. You can easily prevent it, a nullptr check usually gets the job done.

Is it undefined behavior, or is it entirely acceptable, to call delete r on line #2?

It is not UB and entirely acceptable. The delete operator simply calls the IDisposable::Dispose() method and thus runs your destructor. What you do inside it, very typically calling the destructor of an unmanaged class, may well invoke UB.

If we remove line #2, does it matter that r is still a tracking handle

No. Invoking the destructor is entirely optional without a good way to enforce it. Nothing goes wrong, the finalizer ultimately will always run. In the given example that will happen when the CLR runs the finalizer thread one last time before shutting down. The only side effect is that the program runs "heavy", holding on to resources longer than necessary.

Under which other circumstances can the destructor of a managed object be called more than once?

It's pretty common, an overzealous C# programmer may well call your Dispose() method more than once. Classes that provide both a Close and a Dispose method are pretty common in the framework. There are some patterns where it is nearly unavoidable, the case where another class assumes ownership of an object. The standard example is this bit of C# code:

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

The StreamWriter object will take ownership of its base stream and call its Dispose() method at the last curly brace. The using statement on FileStream object calls Dispose() a second time. Writing this code so that this doesn't happen and still provide exception guarantees is too difficult. Specifying that Dispose() may be called more than once solves the problem.

Would it be OK to insert x.~Foo(); immediately before or after r = %x;?

It's okay. The outcome is unlikely to be pleasant, a NullReferenceException would be the most likely result. This is something that you should test for, raise an ObjectDisposedException to give the programmer a better diagnostic. All standard .NET framework classes do so.

In other words, do managed objects "live forever"

No, the garbage collector declares the object dead, and collects it, when it cannot find any references to the object anymore. This is a fail-safe way to memory management, there is no way to accidentally reference a deleted object. Because doing so requires a reference, one that the GC will always see. Common memory management problems like circular references are not an issue either.

Code snippet

Deleting the a object is unnecessary and has no effect. You only delete objects that implement IDisposable, an array does not do so. The common rule is that a .NET class only implements IDisposable when it manages resources other than memory. Or if it has a field of a class type that itself implements IDisposable.

It is furthermore questionable whether you should implement a destructor in this case. Your example class is holding on to a rather modest unmanaged resource. By implementing the destructor, you impose the burden on the client code to use it. It strongly depends on the class usage how easy it is for the client programmer to do so, it definitely is not if the object is expected to live for a long time, beyond the body of a method so that the using statement isn't usable. You can let the garbage collector know about memory consumption that it cannot track, call GC::AddMemoryPressure(). Which also takes care of the case where the client programmer simply doesn't use Dispose() because it is too hard.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...