Click here to Skip to main content
Click here to Skip to main content

RAII Idiom and Managed C++

, 1 Sep 2003
Rate this:
Please Sign up or sign in to vote.
A template wrapper that enables deterministic cleanup in .NET environment.

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");    // open fn for reading and writing
    // use file through f
}

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_;

    // 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:

  • 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

  1. Bjarne Stroustrup: Why doesn't C++ provide a "finally" construct?
  2. Jon Hanna: The RAII Programming Idiom
  3. Andrei Alexandrescu: Modern C++ Design, Addison-Wesley
  4. Tomas Restrepo: Tips and Tricks to Bolster Your Managed C++ Code in Visual Studio .NET
  5. Alex Farber: Writing a Smart Handle class using template template parameters

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Nemanja Trifunovic
Software Developer (Senior) SAP
United States United States
Born in Kragujevac, Serbia. Now lives in Boston area with his wife and daughters.
 
Wrote his first program at the age of 13 on a Sinclair Spectrum, became a professional software developer after he graduated.
 
Very passionate about programming and software development in general.

Comments and Discussions

 
GeneralMy vote of 5 Pingrouparid0814-Jan-13 0:06 
GeneralRe: My vote of 5 PinmemberNemanja Trifunovic14-Jan-13 4:17 
GeneralNice, but a performance note... PinmemberArnaud D11-May-05 11:10 
GeneralRe: Nice, but a performance note... PinmemberNemanja Trifunovic13-May-05 5:05 
GeneralGreat Job, I Have 1 question PinsussAnonymous24-Jun-04 0:59 
GeneralWow! PinsussJon Hanna22-Oct-03 6:11 
GeneralRe: Wow! PinmemberNemanja Trifunovic22-Oct-03 6:37 
GeneralRe: Wow! PinsussJon Hanna22-Oct-03 6:57 
GeneralExcellent article Pinmemberhgrund9-Sep-03 1:42 
GeneralRe: Excellent article PinmemberNemanja Trifunovic9-Sep-03 6:13 
GeneralRe: Excellent article Pinmemberhgrund9-Sep-03 6:49 
GeneralRe: Excellent article PinmemberNemanja Trifunovic9-Sep-03 7:32 
GeneralAnother 5 PinmemberBlake Coverett2-Sep-03 16:18 
GeneralRe: Another 5 PinmemberNemanja Trifunovic2-Sep-03 16:51 
GeneralAnother 5 Pinmemberytkaczyk4-Sep-03 6:53 
GeneralRe: Another 5 PinmemberNemanja Trifunovic4-Sep-03 6:56 
GeneralMy inner mentat says... PinmemberJim Crafton2-Sep-03 14:08 
GeneralRe: My inner mentat says... PinmemberNemanja Trifunovic2-Sep-03 16:53 
GeneralGood article! PinmemberJörgen Sigvardsson2-Sep-03 10:59 
I enjoyed it!
 
--
Keep him tied, it makes him well
He's getting better, can't you tell?

GeneralRe: Good article! PinmemberNemanja Trifunovic2-Sep-03 11:05 
GeneralRe: Good article! PinmemberJörgen Sigvardsson2-Sep-03 11:27 
GeneralRe: Good article! PinmemberRanjan Banerji2-Sep-03 11:32 
GeneralGood article PinmemberJörgen Sigvardsson2-Sep-03 10:58 
GeneralRe: Good article PinmemberNemanja Trifunovic2-Sep-03 11:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 2 Sep 2003
Article Copyright 2003 by Nemanja Trifunovic
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid