JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Deterministic Cleanup

The new managed C++ 2005 (C++/cli) provides the semantics of a managed object on the stack that implements deterministic cleanup. In C++ 2005 a managed class that provides a destructor automagically implements the IDisposable interface. The destructor code is mapped by the compiler to the Dispose method and this destructor code is called when a managed object with stack based semantics goes out of scope. This allows you to implement the RAII (Resource Acquisition Is Initialization) pattern in managed C++.  To implement the RAII pattern in managed C++ you allocate resources dynamically in the constructor, delete them in the destructor and create the object on the stack. Even if an exception is thrown, the destructor will still be called.

If you try to implement a Dispose method the compiler will complain:

error C2605: 'Dispose' : this method is reserved within a managed class

This is due to the fact that the C++ compiler is generating a Dispose method as:

// equivalent Dispose pattern
void Dispose(bool disposing) {
   if (disposing) {
      ~T();
   } else {
      !T();
   }
}

This is necessary since your C++ code may be called from a non C++ managed module or assembly. Here is some sample code for a class named RAII that demonstrates the new deterministic behavior.  In this sample code, the unmanaged resources are released in the finalizer method (!RAII) and the destructor (~RAII) explicity calls the finalizer (!RAII). 

public ref class RAII 
{
private:
	int i;
	bool isDeleted;
public:
	RAII() : i(2), isDeleted(false)
	{
		Console::WriteLine(L"Constructor called.");
	}
	~RAII() // maps to Dispose
	{ 
		if (!isDeleted) { // release once only
			// clean up managed resources here.
			Console::WriteLine(L"Releasing managed resources."); 
			this->!RAII(); // clean up unmanaged resources here.
		}
		this->isDeleted= true;
		Console::WriteLine(L"Destructor called.");
		// finalize (!RAII) is automagically suppressed
	};
	!RAII() // maps to finalizer
	{
		if (!isDeleted) 
		{
			// Clean up unmanaged resources here.
			Console::WriteLine(L"Releasing unmanged resources.");
		}
		Console::WriteLine(L"finalizer called.");
	}
	property int I {
		void set(int value) {this->i = value;}
		int get() {return i;}
	}
	// this can be called even after this object has been disposed!
	// ASSERT isDeleted is false
	void SayHello() 
	{
		if (isDeleted) {
			throw gcnew System::InvalidOperationException();
		}
		Console::WriteLine(L"Hello");
	}
	void Close() {
		if (!isDeleted) {
			this->~RAII();
		}
	}
};

Note: Before you go forward you may want to watch the webcast on deterministic cleanup or read this link.

You can now instantiate and call methods and properties on an object with stack based semantics as follows:

RAII r;
r.SayHello();
Console::WriteLine(r.I);

In C#, you can also implement a form of the RAII by explicitly implementing the IDisposable interface and wrapping the object creation in the using construct. In this sample I used the standard Dispose(bool disposing) pattern.

    class Program : IDisposable
    {
        private int i;
        private bool disposed = false;
        public Program()
        {
            i = 2;
            Console.WriteLine("Constructor called.");
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this); // remove this from gc finalizer list
            Console.WriteLine("Dispose called.");
        }
        // call Dispose(true) from Dispose, call Dispose(false) from finalizer
        private void Dispose(bool disposing)
        {
            if (!this.disposed) // dispose once only
            {
                if (disposing) // called from Dispose
                {
                    // Dispose managed resources.
                }
                // Clean up unmanaged resources here.
            }
            disposed = true;
        }
        ~Program() // maps to finalize
        {
            Dispose(false);
        }
        public void SayHello()
        {
            Console.WriteLine("Hello");
        }
        public int I 
        {
            get{return i;}
            set{i = value;}
        }
        static void Main(string[] args)
        {
            using (Program p = new Program())
            {
                p.SayHello();
                Console.WriteLine(p.I);
            }
            Console.ReadLine();
        }
    }

Managed C++ 2005 frees you from the explicit implementation of IDisposable and the using pattern. To implement RAII in Managed C++ 2005, you simply cleanup resources in the destructor and create an object that "appears to go on the stack". The cleanup code will be called when the object "on the stack" goes out of scope. When the destructor is called, the object is removed from the garbage collector finalizer list so that the finalize method is not called. This is equivalent to the C# call to

GC.SuppressFinalize(this);

To a C# caller outside of the assembly, the class simply appears to implement the IDisposable interface as in:

// C# code
using (RAII r = new RAII())
{
    r.SayHello();
    Console.WriteLine(r.I);
}

Again, even if an exception is thrown in the body of the using statement, Dispose will still be called.

According to the documentation at MSDN:

A Finalize method acts as a safeguard to clean up resources in the event that your Dispose method is not called. You should only implement a Finalize method to clean up unmanaged resources. You should not implement a Finalize method for managed objects, because the garbage collector cleans up managed resources automatically. By default, the Object.Finalize method does nothing. If you want the garbage collector to perform cleanup operations on your object before it reclaims the object's memory, you must override this method in your class.

So, in managed C++ 2005 (C++/cli):
bullet!MyClass() maps to finalizer which, if called by the garbage collector, runs in a separate thread.
bullet~MyClass() maps to Dispose which runs in the same thread. The compiler generated Dispose method calls GC.SuppressFinalize(this).
bulletIf you create an object with a destructor on the stack, the destructor will be called when the object goes out of scope.
bulletWhen the destructor executes, it removes the object from the finalizer list.
bulletIf you declare a destructor or finalizer, the compiler will generate a Dispose method for you.

If you create an object with a destructor on the heap, the destructor is not called by the garbage collector even when it is unreachable from root. You can still explicitly call the destructor on a managed object on the heap using the delete keyword, but there is nothing stopping the caller from invoking method calls on the "deleted" object. If you explicitly call delete, you may want to immediately set the handle to nullptr, so that the caller cannot invoke a method on a "deleted" object.

// explicit destruction
RAII^ heaped= gcnew RAII();
heaped->SayHello();
Console::WriteLine(heaped->I);
delete heaped;
heaped = nullptr; // after delete, don't use this handle

Even this approach is not safe if the execution path throws an exception before the call to delete. An exception safe implementation would call delete on a valid handle in a try finally construct.

RAII^ heaped= nullptr;
try 
{
	heaped= gcnew RAII();
	heaped->SayHello();
	Console::WriteLine(heaped->I);
	//throw gcnew Exception();
}
finally 
{
	if (heaped != nullptr) 
	{
		delete heaped;
		heaped= nullptr;
	}
}

If all of this seems like deja vu, I agree. A managed object with an exception safe destructor "on the stack" smacks of a smart pointer. A smart pointer is a wrapper class that wraps a pointer to an object on the heap. A smart pointer allows access to the underlying resource using the -> and * operators so that it looks like the underlying pointer, an example of the proxy pattern. It is an exception safe way to automatically delete an object on the heap when the smart pointer, an object on the stack, goes out of scope.

As you can see, C++/cli supports deterministic destructors, but only if the object is created "on the stack". This magic is accomplished by mapping to Dispose, finalizer, using and an implicit call to GC.SuppressFinalize(this). 

In C++, if you create the object on the heap, you may fail to call delete or you may try to call a method on a "deleted" object. In C#, if you create this object without the using construct, you may fail to call Dispose or you may try to call a method on a "disposed" object. The take home lesson is to take advantage of the built in mechanisms for releasing resources. In C++, release resources in the destructor and create the object on the stack. In C#, release resources in Dispose and create the object with the using construct.

Although managed objects can be created with value semantics, the actual objects still go on the managed heap. If you need to create or assign a new managed object on the stack from an existing  managed object on the stack you should write a copy constructor and an assignment operator.

// copy constructor
// prefer initializer to assignment in constructors
// list members in initializer list in declared order
RAII(RAII% src):i(src.i),isDeleted(src.isDeleted)
{
    //this->i= src.i;
    //this->isDeleted= src.isDeleted;
    Console::WriteLine("Copy Constructor called.");
}

// assignment operator
RAII% operator=(const RAII% src)
{
    if (this != %src) { // check for self assignment
        this->i= src.i;
        this->isDeleted= src.isDeleted;
    } 

    Console::WriteLine("Assignment operator called.");
    return *this; // support assignment chaining
}

The copy constructor and assignment operator would need to properly copy any unmanaged resources. If you do not implement a copy constructor or assignment operator, or if you make them private, the compiler will trap the invalid operation at compile time.

Finally, you could write a strict version of the RIAA class that throws an exception on raises an ASSERT on a call to !RAII().

Have fun,
Jeff

Note: If you try to wrap multiple unmanaged resources in a class, things get a bit more complicated. In standard C++, object construction must be "transactional." In standard C++ upon failure to completely construct an object, any allocated resources should be released before calling throw. In C++/cli if an exception is thrown in the constructor of a managed class, the destructor will be called, so you will need to code appropriately. Alternatively, in a nothrow approach, the exception can be swallowed and the object marked as invalid, isValid == false;

// TestDestructor.cpp : main project file.
#include "stdafx.h"
using namespace System;
using namespace System::Diagnostics;
// Demonstrates exception safe behaviour in constructor
public class RAII {
private:
	int *p1,*p2;
	int size;
	// disallow assignment make this private
	// we don't know what to do on partially successful
	// assignment
	RAII& RAII::operator=(const RAII& src) {
		Debug::Assert(true,"assignment operator called");
		return *this; 
	}
public:
	// if the constructor does not finish, then
	// the object never existed
	// no resources in initializer list
	RAII::RAII(int size): p1(0), p2(0) ,size(0) {
		try {
			this->size= size;
			p1= new int[size];
			//throw gcnew Exception();  // RAII object never exists
			p2= new int[size];
			//throw gcnew Exception();  // RAII object never exists
		}
		catch(...) {
			if (p1) {delete p1;}
			if (p2) {delete p2;}
			throw;
		}
		// will not get here on exception
		Console::WriteLine("allocated resources");
	}
	virtual RAII::~RAII() {
		if (p1) {
			delete [] p1;
			p1= 0;
		}
		if (p2) {
			delete [] p2;
			p2= 0;
		}
		Console::WriteLine("released resource");
	}
	RAII::RAII(RAII &src) : p1(0), p2(0) ,size(0){
		try {
			size= src.size;
			p1= new int[src.size];
			p2= new int[src.size];
		}
		catch(...){
			if (p1) {delete p1;}
			if (p2) {delete p2;}
			throw;
		}
		Console::WriteLine("copy");
	}
	int Size() {
		return size;
	}
};
int main(array<System::String ^> ^args)
{
	try {
		RAII r(5);
		Console::WriteLine(r.Size());
	}
	catch(...){
		Console::WriteLine("exception");
	}
    Console::ReadLine();
    return 0;
}
 
Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1