2

A colleague of mine came across an article on codeproject over the weekend which describes the use of the auto_handle method. Given that the article was written in 2006, is that still the right way to handle deterministic disposal for things like file handles?

We've tended to explicitly flush and close file handles and then delete the pointers and null them to be certain the GC has no excuse not to collect them (yes, we know we're super-paranoid). Using something like auto_handle looks like it could help us be more lazy (and we like being lazy when it's safe to do so) but we don't want to start using it's considered bad practice these days and/or there's something better we can use.

Jon Cage
  • 36,366
  • 38
  • 137
  • 215

3 Answers3

4

C++/CLI hasn't changed since the 2005 release, so no reason to assume anything new happened. Same advice as was valid back in 2005, auto_handle is not the best way to auto-delete objects. You should consider stack semantics first, it covers the vast majority of cases. The compiler auto-generates the try/finally blocks and the delete call (i.e. disposes the object), equivalent to the C# using statement, minus the code. The example given in the link is done like this:

void Demo1A()
{
    StreamWriter sw("c:\\temp\\test.txt");
    sw.WriteLine("This is a line of text");
}   // <=== Compiler auto-generates the StreamWriter::Dispose call here

Note the missing ^ hat on the object declaration.

And do consider that this has nothing at all to do with the garbage collector, only with deterministic resource cleanup. Also note that setting local variables to nullptr is very inappropriate, it can extend the lifetime of a reference beyond its actual use unnecessarily. The jitter optimizer removes the assignments.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks as always for the clarification Hans. One question though; Why does nullptr extend the lifetime of the reference? I was under the impression that assigning it like that would have precisely the opposite effect? – Jon Cage Jun 18 '12 at 19:12
  • Same idea as GC::KeepAlive(), the jitter tells the collector when a variable is no longer used. By assigning it, you keep it alive longer than necessary. – Hans Passant Jun 18 '12 at 19:19
  • Am I missing something here? Doesn't the reference still point to the object on the heap after you've called delete? Or does the delete call effectively remove the reference and tell the GC it can collect the object when it gets around to it? If we're using stack semantics what you're saying makes sense but if you'd gcnew'ing yourself on a member variable of a class then surely deleting and nullptr'ing the member variable tells the JITter/GCer you're done with an object? – Jon Cage Jun 19 '12 at 08:05
  • You certainly do. The delete operator does *not* modify the reference or heap allocation in any way, it only calls IDisposable::Dispose(). The garbage collector does *not* need you to tell it your "done with an object", it already knows. Unless the reference is a global variable. You got the wrong mental image. A C++ one, not a managed gc one. – Hans Passant Jun 19 '12 at 09:31
  • I'm sure I have the wrong mental map. Are there any books or articles online you'd suggest as some background reading to modify my understanding? – Jon Cage Jun 19 '12 at 09:33
2

Yes, it is still the right way to handle deterministic disposal. Using auto_handle is the C++/CLI equivalent of the C# using statement.

Using auto_handle (which uses RAII) allows you to get exception safety (i.e. the object is disposed even if an exception is thrown) without having to write any try-catch.

See also: C++/CLI Resource Management Confusion

Community
  • 1
  • 1
Matt Smith
  • 17,026
  • 7
  • 53
  • 103
0

There is, I think, a simpler way to do deterministic disposal of managed objects. Simply declare them without a ^, and you'll get the same type of behavior as you would by leaving the * off of an unmanaged C++ class: The constructor is called when the variable is initialized, and the 'destructor' (actually the Dispose method) is called when the variable goes out of scope.

When this is compiled, it turns into a try-catch-dispose if it's a local variable, or it adds a line to that class's Dispose method if it's a class field.

This is not drastically different from the auto_handle, which has a call to Dispose in it's destructor, but I think the syntax is easier to work with.

Examples:

C++/CLI:

public ref class Foo
{
    AutoResetEvent are;

public:
    Foo() : are(false)
    {
        this->are.Set(); // Use a '.' to access members.
    }

    void SomeMethod()
    {
        AutoResetEvent are2(false);
        are2.Set();
    }
};

Equivalent C#, from Reflector, so you can see what it's doing behind the scenes:

public class Foo : IDisposable
{
    // Fields
    private readonly AutoResetEvent modreq(IsByValue) are;

    // Methods
    public Foo()
    {
        AutoResetEvent modopt(IsConst) event2 = new AutoResetEvent(false);
        try
        {
            this.are = event2;
            base..ctor();
            this.are.Set();
        }
        fault
        {
            this.are.Dispose();
        }
    }

    public void ~Foo()
    {
    }

    public sealed override void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    [HandleProcessCorruptedStateExceptions]
    protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
    {
        if (flag1)
        {
            try
            {
                this.~Foo();
            }
            finally
            {
                this.are.Dispose();
            }
        }
        else
        {
            base.Finalize();
        }
    }

    public void SomeMethod()
    {
        AutoResetEvent are2 = null;
        AutoResetEvent modopt(IsConst) event2 = new AutoResetEvent(false);
        try
        {
            are2 = event2;
            are2.Set();
        }
        fault
        {
            are2.Dispose();
        }
        are2.Dispose();
    }
}
David Yaw
  • 27,383
  • 4
  • 60
  • 93
  • Generally speaking it's best to avoid new and gcnew where you can get away with it but often, you're receiving the result of a library function such as `File::CreateText(path);` and if you try and assign that to a non-gcnew'd local variable you get compile errors such as `error C2664: 'System::IO::StreamWriter::StreamWriter(System::IO::Stream ^)' : cannot convert parameter 1 from 'System::IO::StreamWriter ^' to 'System::IO::Stream ^'` – Jon Cage Jun 18 '12 at 16:25