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.
Comments
- Anonymous
March 25, 2004
Why don't you guys use the two-step Dispose pattern using by System.ComponentModel and System.Windows.Forms.
protected virtual Dispose(bool disposing)
{
if (disposing)
{
// disposing means called from Dispose,
// as opposed to Finalize
GC.SuppressFinalize(this);
// dispose managed references
}
// dispose unmanaged resources
base.Dispose(disposing);
}
void IDisposable.Dispose()
{
Dispose(true);
}
~Class()
{
Dispose(false);
} - Anonymous
March 25, 2004
[Continued from above] This way the finalizer will call the appropriate destruction code as needed. - Anonymous
March 25, 2004
The comment has been removed - Anonymous
March 25, 2004
The System.Windows.Forms model of Dispose is broken in the general case, because it assumes that it is safe to run Dispose code within a finalizer.
But, for robust code, you must code your Finalizer methods differently than your Dispose methods because Finalize() is called under different conditions than Dispose(). Finalize() is called in a non-deterministic order, so you can't assume that other references have or haven't already been destroyed.
Its very difficult to correctly code a Finalize() method, because it is impossible to test all the non-deterministic circumstances that it might run under.
My perspective is that Finalize() should only be used to diagnose those conditions when a Dispose() call was missed. So, Finalize() can check to see if the Dispose() call was made, and if it wasn't, it can report an error to help the developer track down the missing Dispose() call.
Implementing Finalize() to call Dispose() automatically would have the effect of actually hiding the programmer error, especially if the Finalize() code works "correctly" and releases resources. Then, after shipping the product, Finalize() gets called in a slightly different way so that it doesn't work properly. Imagine tring to track down that bug in the field. - Anonymous
March 25, 2004
I'm not sure what I do think right now -- but I know I don't like this. Maybe this will help some situations, and I can't pretend to understand enough to argue that point, but its a pretty major change in the basics of the framework -- and it will potentially break, or at least cause leaks, in existing code that is simply ran, or even recompiled, under v2.0 instead of being properly "converted". I'm sorry, but anytime a conversion must be done to prevent leaks . . . - Anonymous
March 25, 2004
- I'm guessing that the new C++ behaviour happens when you compile with /clr:newSyntax so just recompiling woult not affect you. Stan?
2. What Johan described is not new, it is with us since 1.0 ....
- Anonymous
March 25, 2004
FWIW, this way of mapping the destructor to the Dispose pattern is how Delphi 8 for .NET handles this.
For more details see:
http://homepages.borland.com/abauer/archives/2003_11_16_archive.html#106944429821073223 - Anonymous
April 28, 2004
I agree with Johan.
I use a pattern where all classes that need a finalizer must implement the Dispose pattern, and all classes with a Dipose method also have a finalizer; if you have one you must have the other. All clients using the class must call the Dispose method.
In the debug build if a finalizer ever gets called it throws an exception - it means that the client never honored the contract. In the release build we log it and continue.
About being safe during finalization...there are more issues than just which subsystems are still valid (e.g. it may not be safe to call Console.WriteLine during system shutdown), there are also issues related to thread safety. I believe it's possible for a finalizer to run at the same time that the Dispose method is called.
BTW: I'm looking at this from a perspective of pure C#, not managed C++. - Anonymous
June 29, 2004
Hi Stan,
In the VC 2005 Express edition, I cannot seem to create a managed ref class on the heap to take advantage of standard RAII techniques. The compiler seems to think I have to use a ^ to build up my instance. What gives? - Anonymous
July 07, 2004
yes, i know. it did not make it into the beta release. that's disappointing. but it is in the language, and will be there for you when you next get an opportunity towards it. sorry!
stan - Anonymous
June 18, 2009
PingBack from http://outdoordecoration.info/story.php?id=3305