Changes in Destructor Semantics in Support of Deterministic Finalization

In the original language design, a class destructor was permitted within a reference class but not within a value class. This has not changed in the revised V2 language design. 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 V1 code) is the topic of this section. This is probably the most complicated section of the text, so we'll try to go slowly.

Before an object is deleted by the garbage collector, an associated Finalize() method, if present, is invoked. We refer to this as finalization. The timing of just when or whether a Finalize() method is invoke is undefined. This is what is meant when we say that garbage collection exhibits non-deterministic finalization.

When an object maintains a critical resource, perhaps a database connection or a lock, the freeing of this resource cannot depend on the invocation of a finalizer. The canonical solution is to implement the Dispose() method of the System::IDisposable interface. The problem with Dispose() is that it requires an explicit invocation by the user. In the revised language design, the class destructor is used to automate invocation of the Dispose() method.

In the original language, 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 V1 language specification,

__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 that might look like:

// internal transformation of destructor under V1

__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 an virtual destructor. This destructor is what our V1 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 that 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 the revised language design.

In the revised language design, the destructor is renamed internally to the Dispose() method and the reference class is automatically extended to implement the IDispose interface. That is, under V2, our pair of classes are transformed as followed:

// internal transformation of destructor under V2

__gc class A : IDisposable {

public:

   void Dispose() {

      System::GC::SuppressFinalize(this);

Console::WriteLine( "in ~A"); }

   }

};

__gc 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 V2, or when delete is applied to a tracking handle, the underlying Dispose() method is invoked automatically. But what if it is not? Then the garbage collector has no access to the destructor because it is not transformed into Finalize().

To accommodate this possibility, the revised language design provides a revised syntax using the bang (!) to support the explicit definition of a finalizer. For example, one might write

            public ref class R {

      public:

            !R() { Console::WriteLine( "I am the R::finalizer()!" ); }

      };

The !R() method is renamed internally as Finalize(). It is this method, if present, that is invoked by the garbage collector during finalization if the destructor has not been previously invoked. Here is what the transformation might look like:

            // internal transformation under V2

public ref class R {

      public:

      void Finalize()

{ Console::WriteLine( "I am the R::finalizer()!" ); }

      };

 

This has a number of gnarly consequences for V1 code. Essentially what should happen is that an explicit Dispose() method should be transformed into the class destructor. If a destructor is present, it should be transformed into a (!) finalizer. The translation tool currently doesn't do this, but is on the worklist.