Introduction
Experienced C++ programmers make extensive use of RAII (esource initialization is acquisition) idiom [1][2] to manage resources in their programs. For this technique to work, it is necessary to have destructors that are called in a predictable manner. Microsoft�s decision to use a nondeterministic garbage collector for its .NET runtime came as a shock to most C++ programmers, because RAII simply does not work in such environment. While garbage collector takes care of memory, for handling other resources, like files, database connections, kernel objects, etc. it leaves all the job to the programmer. To come up with some kind of solution for this problem, Microsoft introduced methods like Close()
and Dispose()
which work in conjunction with Finalize()
. In C#, there is also a using
keyword, which can be used to automatically call Dispose
(but not Close
) at the end of a scope, but for most of the other .NET languages, it is the programmer�s responsibility to explicitly call one of those two functions after a resource is no longer needed.
In this article, I will explain how it is possible to make a template wrapper that calls either Dispose
or Close
after object goes out of scope, and thus enable C++ programmers to use RAII idiom even when programming in .NET environment. This wrapper is policy-based, and the idea for that came from the excellent Alexandrescu�s book �Modern C++ Design� [3].
Before I started writing this article, I �googled� a while trying to find if someone already came up with this solution. The best I could find was Tomas Restrepo�s auto_dispose
[4], published in February 2002 MSDN Magazine. However, auto_dispose
is a replacement for C# using
keyword, and it is not policy-based, and can not work with Close
method. Also, it requires that objects are derived from IDisposable
interface. Therefore, I am pretty sure I am not reinventing the wheel.
RAII Idiom
To explain RAII idiom, I will use Bjarne Stroustrup�s example [1]:
class File_handle {
FILE* p;
public:
File_handle(const char* n, const char* a)
{ p = fopen(n,a); if (p==0) throw Open_error(errno); }
File_handle(FILE* pp)
{ p = pp; if (p==0) throw Open_error(errno); }
~File_handle() { fclose(p); }
operator FILE*() { return p; }
};
void f(const char* fn)
{
File_handle f(fn,"rw");
}
If we use File_handle
instead of a pointer to FILE
, we don�t need to worry about closing a file. It will automatically close after the File_handle
object goes out of scope. Now, if we strictly follow the rules of structured programming, it is not a big deal to close a file manually after we are done with it. However, .NET applications use exceptions for reporting errors, and it is all but impossible to make well-structured programs with exceptions. Therefore, it is pretty hard to keep track of all the places where a file needs to be closed, and a probability of introducing resource leaks rises. With a File_handle
object created on stack, it�s going to be destroyed and its destructor called automatically when it goes out of scope, and the file will be automatically closed.
Unfortunately, this simple and effective technique does not work with environments controlled by a non-deterministic garbage collector. With __gc
classes, we don�t have destructors that are called in predictable manner, and we can not rely on them to clean up resources after us. However, in MC++ we also have __nogc
classes which do have proper destructors. The obvious idea is to use a __nogc
wrapper and make sure that its destructor calls __gc
object�s Dispose()
or Close()
function.
Class template gc_scoped
To address the problem described above, I have made a class template gc_scoped
, which looks like this:
template <typename T, template <typename>
class CleanupPolicy = DisposeObject>
class gc_scoped : protected CleanupPolicy<T>
{
gcroot <T> object_;
gc_scoped ( const gc_scoped& );
gc_scoped& operator = ( const gc_scoped& );
public:
gc_scoped (const T& object): object_(object){}
~gc_scoped ()
{
Cleanup(object_);
}
T operator-> () const { return object_; }
};
As you can see, this class template takes two template parameters:
T
� which is a __gc*
type.
CleanupPolicy
� a policy class template that specifies the way we clean up our resources. It can be either DisposeObject
(default) which calls Dispose()
, or CloseObject
which calls Close()
.
To see how this class template is useful, let's write a simple function that writes a line of text to a file. Without gc_scoped
, this function would look like this:
void WriteNoRaii (String __gc* path, String __gc* text)
{
StreamWriter __gc* sf;
try
{
sf = File::AppendText(path);
sf->WriteLine(text);
}
__finally
{
if (sf)
sf->Close();
}
}
Note the __finally
block in the example above. We need to manually call Close
in order to close the file. If we forget to do that, we have a resource leak. Now, look at the same example with gc_scoped
:
#include "gc_scoped.h"
void WriteRaii (String __gc* path, String __gc* text)
{
gc_scoped <StreamWriter __gc*, CloseObject> sf (File::AppendText(path));
sf->WriteLine(text);
}
This time we don�t need to manually close our file � gc_scoped
does it for us automatically.
In this example we used the cleanup policy CloseObject
, which called StreamWriter::Close()
internally. For the cases when we want to use Dispose()
, we will specify the cleanup policy DisposeObject
, or just leave out the second template parameter.
The beauty of policy-based designed is that we are not restricted to DisposeObject
and CloseObject
policies at all. If some class implements i.e. function Destroy()
to clean up resources, we can easily write DestroyObject
policy like this:
template <typename T> class DestroyObject
{
protected:
void Cleanup(T object) {object->Destroy();}
};
That's it! Now we can use DestroyObject
policy along with other ones.
Performance penalty
Now, the question that every hardcore C++ programmer will ask is: how much does this cost in terms of performance? For native C++, the compiler is usually able to optimize away all the costs, and to produce the code identical to the one without template wrappers [5]. Here, we have to �double-wrap� our __gc
pointer: first into gc_root
, then into gc_scoped
, and that does not make compiler�s task easier. However, as I ran ILDasm to check the output of WriteRaii
function, I somewhat hoped that VC 7.1 would be able to optimize away gc_scoped
even if it contains a gc_root
member. I was wrong. Here is the output of WriteRaii
:
.method public static void
modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
WriteRaii(string path,
string text) cil managed
{
.vtentry 9 : 1
// Code size 93 (0x5d)
.maxstack 2
.locals
([0] native int V_0,
[1] valuetype [mscorlib]System.Runtime.InteropServices.GCHandle V_1,
[2] valuetype [mscorlib]System.Runtime.InteropServices.GCHandle V_2,
[3] valuetype [mscorlib]System.Runtime.InteropServices.GCHandle V_3,
[4] valuetype 'gc_scoped<System::IO::StreamWriter __gc *,
CloseObject>' sf,
[5] native int V_5)
IL_0000: ldarg.0
IL_0001: call class [mscorlib]System.IO.StreamWriter
[mscorlib]System.IO.File::AppendText(string)
IL_0006: call valuetype
[mscorlib]System.Runtime.InteropServices.GCHandle
[mscorlib]System.Runtime.InteropServices.GCHandle::Alloc(object)
IL_000b: stloc.2
IL_000c: ldloc.2
IL_000d: stloc.1
IL_000e: ldloc.1
IL_000f: call native int
[mscorlib]System.Runtime.InteropServices.GCHandle::op_Explicit
(valuetype [mscorlib]System.Runtime.InteropServices.GCHandle)
IL_0014: stloc.s V_5
IL_0016: ldloca.s sf
IL_0018: ldloca.s V_5
IL_001a: call instance int32 [mscorlib]System.IntPtr::ToInt32()
IL_001f: stind.i4
.try
{
IL_0020: ldloca.s V_0
IL_0022: initobj [mscorlib]System.IntPtr
IL_0028: ldloca.s V_0
IL_002a: ldloca.s sf
IL_002c: ldind.i4
IL_002d: call instance void [mscorlib]System.IntPtr::.ctor(int32)
IL_0032: ldloc.0
IL_0033: call valuetype
[mscorlib]System.Runtime.InteropServices.GCHandle
[mscorlib]System.Runtime.InteropServices.
GCHandle::op_Explicit(native int)
IL_0038: stloc.3
IL_0039: ldloca.s V_3
IL_003b: call instance object
[mscorlib]System.Runtime.InteropServices.
GCHandle::get_Target()
IL_0040: ldarg.1
IL_0041: callvirt instance void
[mscorlib]System.IO.TextWriter::WriteLine(string)
IL_0046: leave.s IL_0055
} // end .try
fault
{
IL_0048: ldsfld int32**
__unep@??1?$gc_scoped@P$AAVStreamWriter
@IO@System@@VCloseObject@@@@$$FQAE@XZ
IL_004d: ldloca.s sf
IL_004f: call void modopt(
[mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
__CxxCallUnwindDtor(method unmanaged thiscall void modopt(
[mscorlib]System.Runtime.CompilerServices.CallConvThiscall)
*(void*), void*)
IL_0054: endfinally
} // end handler
IL_0055: ldloca.s sf
IL_0057: call void modopt([mscorlib]
System.Runtime.CompilerServices.CallConvThiscall)
'gc_scoped<System::IO::StreamWriter __gc *,
CloseObject>.__dtor'
(valuetype 'gc_scoped<System::IO::StreamWriter __gc *,
CloseObject>'*
modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)
modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier))
IL_005c: ret
} // end of method 'Global Functions'::WriteRaii
Compare this to WriteNoRaii
:
.method public static void modopt(
[mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
WriteNoRaii(string path,
string text) cil managed
{
.vtentry 1 : 1
// Code size 29 (0x1d)
.maxstack 4
.locals ([0] class [mscorlib]System.IO.StreamWriter sf)
IL_0000: ldnull
IL_0001: stloc.0
.try
{
IL_0002: ldarg.0
IL_0003: call class [mscorlib]System.IO.StreamWriter
[mscorlib]System.IO.File::AppendText(string)
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: ldarg.1
IL_000b: callvirt instance void
[mscorlib]System.IO.TextWriter::WriteLine(string)
IL_0010: leave.s IL_001c
} // end .try
finally
{
IL_0012: ldloc.0
IL_0013: brfalse.s IL_001b
IL_0015: ldloc.0
IL_0016: callvirt instance void
[mscorlib]System.IO.StreamWriter::Close()
IL_001b: endfinally
} // end handler
IL_001c: ret
} // end of method 'Global Functions'::WriteNoRaii
As you can see, compiler was not able to optimize away GCHandles
, and to my surprise it didn�t even inline gc_scoped
destructor. Therefore, I expected some performance penalty, but how much exactly? To answer this question, I ran both functions 100,000 times. The version with WriteRaii
took approximately 20% more time than the version with WriteNoRaii
.
Therefore, the performance of gc_scoped
turned out to be pretty disappointing. However, giving up gc_scoped
altogether for the sake of performance would fit Knuth�s definition of premature optimization. While there are cases where performance cost of using gc_scoped
would be unacceptable (I wouldn�t recommend using it inside of a tight loop) in many cases the benefits of automatic resource management will be more important.
Conclusion
RAII is a powerful and simple idiom that makes resource management much easier. With gc_scoped
class template, it is possible to use RAII with __gc
types. However, unlike with native C++, there is a performance penalty that may or may not be significant in your applications.
References
- Bjarne Stroustrup: Why doesn't C++ provide a "finally" construct?
- Jon Hanna: The RAII Programming Idiom
- Andrei Alexandrescu: Modern C++ Design, Addison-Wesley
- Tomas Restrepo: Tips and Tricks to Bolster Your Managed C++ Code in Visual Studio .NET
- Alex Farber: Writing a Smart Handle class using template template parameters