|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionExperienced 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 In this article, I will explain how it is possible to make a template wrapper that calls either 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 RAII IdiomTo 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"); // open fn for reading and writing // use file through f } If we use Unfortunately, this simple and effective technique does not work with environments controlled by a non-deterministic garbage collector. With Class template gc_scopedTo address the problem described above, I have made a class template template <typename T, template <typename> class CleanupPolicy = DisposeObject> class gc_scoped : protected CleanupPolicy<T> { gcroot <T> object_; // Non - copyable 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:
To see how this class template is useful, let's write a simple function that writes a line of text to a file. Without 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 #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 – In this example we used the cleanup policy The beauty of policy-based designed is that we are not restricted to template <typename T> class DestroyObject { protected: void Cleanup(T object) {object->Destroy();} }; That's it! Now we can use Performance penaltyNow, 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 .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 .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 Therefore, the performance of ConclusionRAII is a powerful and simple idiom that makes resource management much easier. With References
|
||||||||||||||||||||||