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

Deterministic Destruction in C++/CLI

, 12 Aug 2004
Rate this:
Please Sign up or sign in to vote.
Looks at how deterministic destruction is possible with C++/CLI

Introduction

Many C++ programmers were rather unhappy with the non-deterministic finalization feature they were provided with by the .NET Garbage Collection algorithm. C++ programmers were so much used to the RAII (Resource Acquisition Is Initialization) idiom where they expected a destructor to get called when an object went out of scope or when delete was explicitly called on it, that a non-deterministic destructor simply didn't fit their expectations or requirements. Microsoft alternatively offered the Dispose pattern where classes had to implement IDisposable and then call Dispose on their objects when they went out of scope. The basic issue here was that this required the programmer to manually and consistently call Dispose whenever the object needed to be finalized and it became worse when the object had managed member objects that themselves would need to have Dispose called on them, which then meant they too needed to implement IDisposable. Tiresome sounding, isn't it?

Guess what? In C++/CLI, the Microsoft VC++ team is giving us a destructor that internally gets compiled to the Dispose method and the old finalizer gets an alternate syntax, so we basically have finalizers and destructors as two separate entities that behave differently as they should have in the previous version. The designers of C# made the unfortunate initial mistake of calling their finalizer a destructor and I presume there must be tens of thousands of C# coders out there who have no inkling of the fact that they have got a basic concept in object life-time maintenance absolutely confused with the wrong thing.

Note

It's easy to wrongly call automatic objects as stack objects in C++/CLI, but it should be remembered that the seemingly stack based objects actually reside on the CLR heap, as they are still normal garbage collected ref objects. It's a C++ compiler trick that allow us to treat these variables just as we used to treat stack based objects in unmanaged C++ during the good old days.

The new syntax

In C++/CLI, destructors follow the same syntax used in the pre-managed times, where ~classname would be the method name for the destructor. It also brings out a new naming syntax, !classname which is the method name for the finalizer. Here is what a typical class would look like :-

ref class R1
{
public:
    R1()
    {
        Show("R1::ctor");
    }
    ~R1()
    {
        Show("R1::dtor");
    }
protected:
    !R1()
    {
        Show("R1::fnzr");
    }    
};

The destructor (~R1) gets compiled into a Dispose method in the generated IL.

.method public newslot virtual 
        final instance void 
        Dispose() cil managed
{
  .override [mscorlib]System.IDisposable::Dispose
  // Code size       17 (0x11)
  .maxstack  1
  IL_0000:  ldstr      "R1::dtor"
  IL_0005:  call       void [mscorlib]
        System.Console::WriteLine(string)
  IL_000a:  ldarg.0
  IL_000b:  call       void [mscorlib]
        System.GC::SuppressFinalize(object)
  IL_0010:  ret
} // end of method R1::Dispose

The C# equivalent of the above would be :-

public void Dispose()//IDisposable::Dispose
{
      Console.WriteLine("R1::dtor");
      GC.SuppressFinalize(this);
}
 

There is a call made to GC::SuppressFinalize in the generated Dispose method. This is done to ensure that the finalizer does not get called during the garbage collection cycle that claims this object's memory. If that sounds confusing, remember that we are still restricted by the environment which we are targeting, which happens to be the CLR. In the CLR, reference objects are allocated on the CLR heap and their memory is reclaimed when they are out of use by the Garbage Collector, there is no way the programmer can free up the memory on his/her own. So, even if our destructor gets called, the memory will be released only during the next GC cycle and at that point we don't want the GC trying to call Finalize on our object. GC::SuppressFinalize basically removes the object from the finalization queue.

How it's implemented

void _tmain()
{   
    R1 r;
}

I've declared r as an automatic variable. Now let's see the IL that gets generated for this :-

.method public static int32  
        main() cil managed
{
  .vtentry 1 : 1
  // Code size       16 (0x10)
  .maxstack  1
  .locals (class R1 V_0)
  IL_0000:  ldnull
  IL_0001:  stloc.0
  IL_0002:  newobj     instance void R1::.ctor()
  IL_0007:  stloc.0
  IL_0008:  ldloc.0
  IL_0009:  call       instance void R1::Dispose()
  IL_000e:  ldc.i4.0
  IL_000f:  ret
} // end of method 'Global Functions'::main

The C# equivalent for that would be :-

public static int main()
{
      R1 r = null;
      r = new R1();
      r.Dispose();
      return 0;
}

Pretty straightforward stuff as you can see with Dispose being called when the object goes out of scope. You might be a little surprised that there is no try-catch block in there, but that's because our code fragment was too simple. try-catch blocks are used only if they are required, in the above case, it's not so. Let's see the following code snippet :-

void _tmain()
{   
    R1 r;
    int y=100;
}

The IL generated :-

.method public static int32 
        main() cil managed
{
  .vtentry 1 : 1
  // Code size       28 (0x1c)
  .maxstack  1
  .locals (class R1 V_0,
           int32 V_1)
  IL_0000:  ldnull
  IL_0001:  stloc.0
  IL_0002:  newobj     instance void R1::.ctor()
  IL_0007:  stloc.0
  .try
  {
    IL_0008:  ldc.i4.s   100
    IL_000a:  stloc.1
    IL_000b:  leave.s    IL_0014
  }  // end .try
  fault
  {
    IL_000d:  ldloc.0
    IL_000e:  call       instance void R1::Dispose()
    IL_0013:  endfinally
  }  // end handler
  IL_0014:  ldloc.0
  IL_0015:  call       instance void R1::Dispose()
  IL_001a:  ldc.i4.0
  IL_001b:  ret
} // end of method 'Global Functions'::main

The moment the compiler realizes that there is a probable contingency where control might not reach the line that calls Dispose, it implements a try block and in case of any exception, calls Dispose within the fault handler. The C# equivalent would be :-

public static int main()
{
      R1 r = null;      
      int y;
      r = new R1();
      try
      {
            y = 100;
      }
      catch
      {
            r.Dispose();
      }
      r.Dispose();
      return 0;
}

You could also declare the object as a handle object and then manually call delete on it which equates to calling Dispose on your object.

void _tmain()
{      
   R1^ r = gcnew R1();   
   delete r;   
}

The generated IL is a little more complex for this case (I am not fully sure why an unnecessary int variable is introduced for instance.)

.method public static int32  
        main() cil managed
{
  .vtentry 1 : 1
  // Code size       27 (0x1b)
  .maxstack  1
  .locals (class [mscorlib]System.IDisposable V_0,
           class R1 V_1,
           int32 V_2)
  IL_0000:  ldnull
  IL_0001:  stloc.1
  IL_0002:  newobj     instance void R1::.ctor()
  IL_0007:  stloc.1
  IL_0008:  ldloc.1
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  brfalse.s  IL_0017
  IL_000d:  ldloc.0
  IL_000e:  callvirt   
    instance void [mscorlib]System.IDisposable::Dispose()
  IL_0013:  ldnull
  IL_0014:  stloc.2
  IL_0015:  br.s       IL_0019
  IL_0017:  ldnull
  IL_0018:  stloc.2
  IL_0019:  ldc.i4.0
  IL_001a:  ret
} // end of method 'Global Functions'::main

As I mentioned, I am truly puzzled by the V_2 int32 variable. Here is the C# equivalent for those of you who don't like looking at IL.

public static int main()
{
      int v2;
      R1 r = null;
      r = new R1();
      IDisposable d = r;
      if (disposable1 != null)
      {
            d.Dispose();
            v2  = 0;
      }
      else
      {
            v2 = 0;
      }
      return 0;
}

My best guess is that this is to help the CLR Execution Engine do run-time optimizations; in the above case, the entire if loop might possibly be skipped if r is not null.

How member objects are handled

See the following code snippet :-

#define Show(x) Console::WriteLine(x)
ref class R1
{
public:
   R1()
   {
      Show("R1::ctor");
   }
   ~R1()
   {
      Show("R1::dtor");
   }
protected:
   !R1()
   {
      Show("R1::fnzr");
   }   
};
ref class R
{
public:
   R()
   {
      Show("R::ctor");
   }
   ~R()
   {
      Show("R::dtor");
   }
   R1 r;
protected:
   !R()
   {
      Show("R::fnzr");
   }   
};

Let's take a look at R's constructor in the generated IL :-

.method public specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // Code size       28 (0x1c)
  .maxstack  2
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  ldarg.0
  IL_0007:  newobj     instance void R1::.ctor()
  IL_000c:  stfld      class R1 modopt(
      [Microsoft.VisualC]Microsoft.VisualC.IsByValueModifier) R::r
  IL_0011:  ldstr      "R::ctor"
  IL_0016:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001b:  ret
} // end of method R::.ctor

Equivalent C# code would be :-

public R()
{
      this.r = ((R1 modopt(Microsoft.VisualC.IsByValueModifier)) new R1());
      Console.WriteLine("R::ctor");
} 

The compiler inserts a custom modopt modifier into the instantiation of the R1 object which would give the JIT compiler some idea of how to treat it. In this case, it has marked it with Microsoft.VisualC.IsByValueModifier which presumably means that this object is to be treated as a pass-by-value object. Anyway, that's beyond the scope of this article and what I wanted to put forth here is that the R object's constructor also instantiates and constructs the R1 member object.

Now let's see the R class destructor :-

.method public newslot virtual final instance void 
        Dispose() cil managed
{
  .override [mscorlib]System.IDisposable::Dispose
  // Code size       42 (0x2a)
  .maxstack  1
  .try
  {
    IL_0000:  ldstr      "R::dtor"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  leave.s    IL_0018
  }  // end .try
  fault
  {
    IL_000c:  ldarg.0
    IL_000d:  ldfld      class R1 modopt(
       [Microsoft.VisualC]Microsoft.VisualC.IsByValueModifier) R::r
    IL_0012:  call       instance void R1::Dispose()
    IL_0017:  endfinally
  }  // end handler
  IL_0018:  ldarg.0
  IL_0019:  ldfld      class R1 modopt(
       [Microsoft.VisualC]Microsoft.VisualC.IsByValueModifier) R::r
  IL_001e:  call       instance void R1::Dispose()
  IL_0023:  ldarg.0
  IL_0024:  call       void [mscorlib]System.GC::SuppressFinalize(object)
  IL_0029:  ret
} // end of method R::Dispose

Equivalent C# code is :-

public void Dispose()
{
      try
      {
            Console.WriteLine("R::dtor");
      }
      catch
      {
            this.r.Dispose();
      }
      this.r.Dispose();
      GC.SuppressFinalize(this);
}
 

As you can see, Dispose is called on the member object as well. The compiler sure does generate a lot of code for us, eh?

In the above discussed case, the member object was also an automatic variable. But what if we had a handle variable as a member? In that case, we should manually delete the member variable in our destructor, otherwise there won't be so much benefit out of the deterministic destruction if the member objects will then have to wait for an unpredictable GC cycle before they get disposed. So, this is what we need to do for such cases :-

ref class R
{
public:
    R()
    {
        r = gcnew R1();
        Show("R::ctor");
    }
    ~R()
    {
        delete r;
        Show("R::dtor");
    }
    R1^ r;
protected:
    !R()
    {
        Show("R::fnzr");
    }    
};

Warning

Do not delete member objects manually from your finalizer, because there is every chance that by the time the finalizer is called on your object, its member objects might already have been finalized.

Performance boost

By using destructors whenever possible instead of finalizers, you would see a small-to-medium performance boost in your code. Problem with finalizers it that, the GC promotes objects that need to be finalized to at least Generation 2, and then the finalizer thread will have to run the Finalize method on objects that need finalizatioon, and then the GC has to reclaim the memory in a future cycle.

Points to remember when using destructors

  • You cannot have a method named Dispose in your class, for obvious reasons
  • ~classname is the destructor and !classname is the finalizer
  • Destructors get called when the object goes out of scope, but the memory won't be freed up until the next GC cycle
  • The destructor and finalizer won't get called for the same object
  • For automatic member variables you don't need to do anything special
  • For handle member variables, make sure to delete them manually in the destructor

Conclusion

Essentially the C++/CLI deterministic destructor implementation is internally a syntactically pleasant form of the Dispose-Pattern and the compiler generates just about all the code that we require. I believe C# 2.0 has a slightly inferior form where they use the using-keyword. The big plus about the C++/CLI destructor syntax is that it fits in naturally to what a native C++ programmer expects his/her destructor to do, and he/she needn't even be aware of the Dispose pattern that's being used internally. Thanks to Herb Sutter and his team Smile | :)

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

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
GeneralOn why the int variable is declared in MSIL PinmemberRenjith mohan2-Nov-07 0:23 
GeneralSlight errors in "C# equivalent" code Pinmemberrb1262-May-06 0:53 
GeneralQuestion of the structure on C++/CLI PinmemberAntti Keskinen21-Aug-04 11:05 
GeneralRe: Question of the structure on C++/CLI PinstaffNishant S22-Aug-04 6:51 
GeneralRe: Question of the structure on C++/CLI PinmemberAntti Keskinen23-Aug-04 6:21 
GeneralBasic Question PinmemberJörgen Sigvardsson18-Aug-04 0:30 
GeneralRe: Basic Question PinstaffNishant S18-Aug-04 0:51 
QuestionWhen would you use a finalizer? PinmemberDon Clugston17-Aug-04 14:11 
Pardon my ignorance, but I can't understand what you would use finalizers for once you have proper destructors.
In standard C++, I've personally never used destructors for anything other than RAII. I thought that finalizers were a poor man's destructors.
 
Since you mention that performance should be better with destructors than with finalizers, I'm baffled as to why finalizers need to exist at all.
Have I missed something?
 

AnswerRe: When would you use a finalizer? PinstaffNishant S17-Aug-04 17:13 
GeneralRe: When would you use a finalizer? PinmemberDon Clugston17-Aug-04 21:17 
GeneralRe: When would you use a finalizer? PinstaffNishant S17-Aug-04 22:28 
GeneralAccessing a disposed object/dangling reference PinmemberJörgen Sigvardsson14-Aug-04 4:52 
GeneralRe: Accessing a disposed object/dangling reference PinmemberStephane Lajoie14-Aug-04 15:16 
GeneralRe: Accessing a disposed object/dangling reference PinstaffNishant S18-Aug-04 0:57 
GeneralRe: Accessing a disposed object/dangling reference PinmemberNemanja Trifunovic16-Aug-04 5:25 
GeneralRe: Accessing a disposed object/dangling reference PinstaffNishant S17-Aug-04 22:37 
GeneralRe: Accessing a disposed object/dangling reference PinmemberJörgen Sigvardsson17-Aug-04 22:44 
GeneralRe: Accessing a disposed object/dangling reference PinstaffNishant S18-Aug-04 0:22 
GeneralRe: Accessing a disposed object/dangling reference PinmemberJörgen Sigvardsson18-Aug-04 0:27 
GeneralRe: Accessing a disposed object/dangling reference PinstaffNishant S18-Aug-04 0:37 
GeneralRe: Accessing a disposed object/dangling reference Pinmembermsalters18-Aug-04 21:47 
GeneralC++ 2005 Beta 2 PinsussAnonymous12-Aug-04 22:58 
GeneralRe: C++ 2005 Beta 2 PinstaffNishant S12-Aug-04 23:11 
GeneralRe: C++ 2005 Beta 2 PinmemberTanveer Badar28-Jan-05 5:16 

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 | Mobile
Web03 | 2.8.141022.2 | Last Updated 13 Aug 2004
Article Copyright 2004 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid