< Class, Object & Managed Code 6 | Main | Class, Object & Managed Code 8 >


 

 

Object-Oriented Programming:

Class, Object and Managed Code 7

 

 

What we have in this page?

  1. Changes in Destructor Semantics

  2. Non-deterministic Finalization

  3. In Managed Extensions for C++, Destructors go to Finalize()

  4. In New Syntax, Destructors go to Dispose()

  5. Declaring a Reference Object

 

 

Changes in Destructor Semantics

 

Semantics for class destructors have changed significantly from Managed Extensions for C++ to Visual C++ 2005. In Managed Extensions, a class destructor was permitted within a reference class but not within a value class. This has not changed in the new syntax. However, the semantics of the class destructor have changed considerably. The what and why of that change (and how it impacts the translation of existing CLR code) is the topic of this section. This is probably the most complicated section of the text, so we'll try to go slowly. It is also probably the most important programmer-level change between the two versions of the language, and so it is worth the effort to walk through the material in a stepwise fashion.

 

Non-deterministic Finalization

 

Before the memory associated with an object is reclaimed by the garbage collector, an associated Finalize() method, if present, is invoked. You can think of this method as a kind of super-destructor since it is not tied to the program lifetime of the object. We refer to this as finalization. The timing of just when or even whether a Finalize() method is invoked is undefined. This is what is meant when we say that garbage collection exhibits non-deterministic finalization. Non-deterministic finalization works well with dynamic memory management. When available memory gets sufficiently scarce, the garbage collector kicks in and things pretty much just work. Under a garbage collected environment, destructors to free memory are unnecessary. It's kind of spooky when you first implement an application and don't fret over each potential memory leak. Acclimation comes easy, however.

Non-deterministic finalization does not work well, however, when an object maintains a critical resource such as a database connection or a lock of some sort. In this case, we need to release that resource as soon as possible. In the native world, that is done through the pairing of a constructor/destructor pair. As soon as the lifetime of the object ends, either through the completion of the local block within which it is declared or through the unraveling of the stack because of a thrown exception, the destructor kicks in and the resource is automatically released. It works very well, and its absence under Managed Extensions was sorely missed.

The solution provided by the CLR is for a class to implement the Dispose() method of the IDisposable interface. The problem here is that Dispose() requires an explicit invocation by the user. This is error-prone and therefore a step backwards. The C# language provides a modest form of automation through a special using statement. The Managed Extensions design, as already mentioned, provided no special support at all.

 

In Managed Extensions for C++, Destructors go to Finalize()

 

In Managed Extensions, the destructor of a reference class is implemented through the following two steps:

 

1.   The user-supplied destructor is renamed internally to Finalize(). If the class has a base class (remember, under the CLR Object Model, only single inheritance is supported), the compiler injects a call of its finalizer following execution of the user-supplied code. For example, given the following trivial hierarchy taken from the Managed Extensions language specification:

// Managed Extensions C++ code...

__gc class A

{

   public:

     ~A() { Console::WriteLine(S"In ~A"); }

};

  

__gc class B : public A

{

   public:

     ~B() { Console::WriteLine(S"In ~B");  }

};

both destructors are renamed Finalize(). B's Finalize() has an invocation of A's Finalize() method added following the invocation of WriteLine(). This is what the garbage collector will by default invoke during finalization. Here is what this internal transformation might look like:

// internal transformation of destructor under Managed Extensions

__gc class A

{

   public:

         void Finalize() { Console::WriteLine(S"In ~A"); }

};

 

__gc class B : public A

{

   public:

         void Finalize()

         {

               Console::WriteLine(S"In ~B");

               A::Finalize();

         }

};

2.  In the second step, the compiler synthesizes a virtual destructor. This destructor is what our Managed Extensions user programs invoke either directly or through an application of the delete expression. It is never invoked by the garbage collector.

What is placed within this synthesized destructor? Two statements. One is a call to GC::SuppressFinalize() to make sure there are no further invocations of Finalize(). The second is the actual invocation of Finalize(). This, recall, represents the user-supplied destructor for that class. Here is what this might look like:

__gc class A

{

      public:

            virtual ~A()

            {

                  System::GC::SuppressFinalize(this);

                  A::Finalize();

            }

};

 

__gc class B : public A

{

      public:

            virtual ~B()

            {

                  System::GC::SuppressFinalize(this);

                  B::Finalize();

            }

};

While this implementation allows the user to explicitly invoke the class Finalize() method now rather than whenever, it does not really tie in with the Dispose() method solution. This is changed in Visual C++ 2005.

 

 

 

 

In New Syntax, Destructors go to Dispose()

 

In the new syntax, the destructor is renamed internally to the Dispose() method and the reference class is automatically extended to implement the IDispose interface. That is, under Visual C++ 2005, our pair of classes is transformed as follows:

// internal transformation of destructor under the new syntax

ref class A : IDisposable

{

      public:

            void Dispose()

            {

                  System::GC::SuppressFinalize(this);

                  Console::WriteLine( "In ~A");

            }

};

 

ref class B : public A

{

      public:

            void Dispose()

            {

                  System::GC::SuppressFinalize(this);

                  Console::WriteLine("In ~B");

                  A::Dispose();

            }

};

When either a destructor is invoked explicitly under the new syntax, or when delete is applied to a tracking handle, the underlying Dispose() method is invoked automatically. If it is a derived class, a call of the Dispose() method of the base class is inserted at the close of the synthesized method. But this doesn't get us all the way to deterministic finalization. In order to reach that, we need the additional support of local reference objects. This has no analogous support within Managed Extensions, and so it is not a translation issue.

 

 

 

 

 

 

Declaring a Reference Object

 

Visual C++ 2005 supports the declaration of an object of a reference class on the local stack or as a member of a class as if it were directly accessible. When combined with the association of the destructor with the Dispose() method as described above, the result is the automated invocation of finalization semantics on reference types. First, we define our reference class such that object creation functions as the acquisition of a resource through its class constructor. Secondly, within the class destructor, we release the resource acquired when the object was created.

public ref class R

{

      public:

            R() { /* acquire expensive resource */ }

            ~R() { /* release expensive resource */ }

            // ...everything else...

};

The object is declared locally using the type name but without the accompanying hat. All uses of the object, such as invoking a member function, are done through the member selection dot (.) rather than arrow (->). At the end of the block, the associated destructor, transformed into Dispose(), is invoked automatically

void f()

{

      R r;

      r.methodCall();

      // r is automatically destructed here –

      // that is, r.Dispose() is invoked

}

As with the using statement within C#, this is syntactic sugar rather than defiance of the underlying CLR constraint that all reference types must be allocated on the CLR heap. The underlying semantics remain unchanged. The user could equivalently have written the following (and this is likely the internal transformation carried out by the compiler):

// equivalent implementation

// except that it should be in a try/finally clause

void f()

{

      R^ r = gcnew R;

      r->methodCall();

      delete r;

}

In effect, under the new syntax, destructors are once again paired with constructors as an automated acquisition/release mechanism tied to a local object's lifetime. This is a significant and quite astonishing accomplishment and the language designers should be roundly applauded for this.

 

 

 

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 | Part 7 | Part 8

 

 


< Class, Object & Managed Code 6 | Main | Class, Object & Managed Code 8 >