First, this writing concentrates of and compares between three programming languages, C#, C++/CLI, and ISO/ANSI C++. It discusses 9 rules that every developer should keep in mind while working with constructors, destructors, and finalizers and class hierarchies:
Rule #1: Constructors are called in descending order; starting from the root class and stepping down through the tree to reach the last leaf object that you need to instantiate. Applies to C#, C++/CLI, and ANSI C++.
Let’s consider a simple class hierarchy like this:
class BaseClass { public BaseClass() { Console.WriteLine("ctor of BaseClass"); } } class DerivedClass : BaseClass { public DerivedClass() { Console.WriteLine("ctor of DerivedClass"); } } class ChildClass : DerivedClass { public ChildClass() { Console.WriteLine("ctor of ChildClass"); } }
ChildClass inherits from DerivedClass, and DerivedClass, in turn, inherits from BaseClass.
When we create a new instance of ChildClass using a simple line like this:
static void Main() { ChildClass cls = new ChildClass(); }
the code outputs the following results:
ctor of BaseClass ctor of DerivedClass ctor of ChildClass
Rule #2: In C# lexicology, a destructor and a finalizer refer to the same thing; the function called before the object is fully-removed from the memory (i.e. GC-collected). Applies to C# only.
Let’s consider the same class hierarchy but with destructors:
class BaseClass { public ~BaseClass() { Console.WriteLine("dtor of BaseClass"); } } class DerivedClass : BaseClass { public ~DerivedClass() { Console.WriteLine("dtor of DerivedClass"); } } class ChildClass : DerivedClass { public ~ChildClass() { Console.WriteLine("dtor of ChildClass"); } }
When you define a class destructor with that C++-alike syntax (preceding the function name with a ~) the compiler actually replaces your destructor with an override of the virtual Object.Finalize() function. That is, before the object is removed (i.e. GC-collected) from the memory, the finalizer (i.e. destructor) is called first. This finalizer first executes your code. After that it calls the finalizer of the base type of your object. If we could decompile our assembly, we would see that our destructor in the ChildClass (so other classes) has been replaced with this function:
protected virtual void Finalize() { try { Console.WriteLine("dtor of ChildClass"); } finally { base.Finalize(); } }
Rule #3: Destructors are called in ascending order, starting from the leaf object that you need to instantiate and moving up through the tree to reach the very first base class of your object. In reverse of constructors calling order. Applies to C#, C++/CLI, and ANSI C++.
Now, instantiate your class:
static void Main() { ChildClass cls = new ChildClass(); // 'cls' is removed from memory here }
the code should outputs the following results:
dtor of ChildClass dtor of DerivedClass dtor of BaseClass
Rule #4: Finalizers are a feature of GC managed objects (i.e. managed classes). That’s because the finalizer is called only when the GC removes the object from the memory (i.e. frees memory associated with).
Now, try to create a simple structure with a destructor:
struct MyStruct { ~MyStruct() { Console.WriteLine("dtor of MyStruct"); } }
The code won’t compile. That’s because that GC doesn’t handle structures.
That’s because you don’t know when the next garbage collection would occur, even if you performed a manual garbage collection (using System.GC.Collect() function) you won’t know exactly when memory would be released. In addition, GC always delay releasing of finalizable object, it puts them in a special GC queue called freachable (pronounced ef-reachable, F stands for Finalize) queue. Applies to C# and C++/CLI (.NET.)
Rule #6: C++/CLI differs between destructors and finalizers. That is, finalizers are called by GC, and destructors are called when you manually delete the object.
Let’s consider the same example but in C++/CLI:
ref class BaseClass { public: BaseClass() { Console::WriteLine("ctor of BaseClass"); } ~BaseClass() { Console::WriteLine("dtor of BaseClass"); GC::ReRegisterForFinalize(this); } }; ref class DerivedClass : BaseClass { public: DerivedClass() { Console::WriteLine("ctor of DerivedClass"); } ~DerivedClass() { Console::WriteLine("dtor of DerivedClass"); GC::ReRegisterForFinalize(this); } }; ref class ChildClass : DerivedClass { public: ChildClass() { Console::WriteLine("ctor of ChildClass"); } ~ChildClass() { Console::WriteLine("dtor of ChildClass"); GC::ReRegisterForFinalize(this); } };
When we run the code:
int main() { ChildClass^ cls = gcnew ChildClass(); }
it outputs the following results:
ctor of BaseClass ctor of DerivedClass ctor of ChildClass
The destructors are not called. Why? Unlike C#, in C++/CLI there is a big difference between destructors and finalizers. As you know, the finalizer is called when the GC removes the object from the memory. Destructors, on the other hand, are called when you destroy the object yourself (e.g. use the delete keyword.)
Now, try to change the test code to the following:
int main() { ChildClass^ cls = gcnew ChildClass(); delete cls; }
Run the code. Now, destructors are called.
Next, let’s add finalizers to our objects. The code should be like the following:
ref class BaseClass { public: BaseClass() { Console::WriteLine("ctor of BaseClass"); } ~BaseClass() { Console::WriteLine("dtor of BaseClass"); GC::ReRegisterForFinalize(this); } !BaseClass() { Console::WriteLine("finz of BaseClass"); } }; ref class DerivedClass : BaseClass { public: DerivedClass() { Console::WriteLine("ctor of DerivedClass"); } ~DerivedClass() { Console::WriteLine("dtor of DerivedClass"); GC::ReRegisterForFinalize(this); } !DerivedClass() { Console::WriteLine("finz of DerivedClass"); } }; ref class ChildClass : DerivedClass { public: ChildClass() { Console::WriteLine("ctor of ChildClass"); } ~ChildClass() { Console::WriteLine("dtor of ChildClass"); GC::ReRegisterForFinalize(this); } !ChildClass() { Console::WriteLine("finz of ChildClass"); } };
As you see, the syntax of constructors, destructors, and finalizers are very similar.
Now, let’s try the code:
int main() { ChildClass^ cls = gcnew ChildClass(); }
GC would call finalizers and the code would outputs the following:
ctor of BaseClass ctor of DerivedClass ctor of ChildClass finz of ChildClass finz of DerivedClass finz of BaseClass
Now, try to destroy the object yourself:
int main() { ChildClass^ cls = gcnew ChildClass(); delete cls; }
The delete statement calls object destructors and removes the object from memory.
Or else, declare the object with stack-semantics:
int main()
{
ChildClass cls;
}
Now, destructors are called when the scope of the object ends.
Rule #8: In C++/CLI, destructors and finalizers are not called together. Only destructors or finalizers are called. If you manually delete the object or you declare it with stack-semantics, destructors are called. If you leaved the object for GC to handle, finalizers are called.
Now try to run the code. The code should outputs the following results:
ctor of BaseClass ctor of DerivedClass ctor of ChildClass dtor of ChildClass dtor of DerivedClass dtor of BaseClass
Rule #9: Beware of virtual (overridable) functions in constructors. In .NET (C# and C++/CLI,) the overload of the most derived object (the object to be instantiated) is called. In traditional C++ (ISO/ANSI C++,) the overload of the current object constructed is called.
Let’s update our C# example:
class BaseClass { public BaseClass() { Foo(); } public virtual void Foo() { Console.WriteLine("Foo() of BaseClass"); } } class DerivedClass : BaseClass { public DerivedClass() { } public override void Foo() { Console.WriteLine("Foo() of DerivedClass"); } } class ChildClass : DerivedClass { public ChildClass() { } public override void Foo() { Console.WriteLine("Foo() of ChildClass"); } }
When you execute the code:
static void Main() { ChildClass cls = new ChildClass(); }
you would get the following results:
Foo() of ChildClass
The same code in C++/CLI:
ref class BaseClass { public: BaseClass() { Foo(); } virtual void Foo() { Console::WriteLine("Foo() of BaseClass"); } }; ref class DerivedClass : BaseClass { public: DerivedClass() { } virtual void Foo() override { Console::WriteLine("Foo() of DerivedClass"); } }; ref class ChildClass : DerivedClass { public: ChildClass() { } virtual void Foo() override { Console::WriteLine("Foo() of ChildClass"); } };
The code outputs the same results.
But what if you need to call the virtual function of the BaseClass? Just change the code to the following:
ref class BaseClass { public: BaseClass() { BaseClass::Foo(); } virtual void Foo() { Console::WriteLine("Foo() of BaseClass"); } };
Now, the code outputs:
Foo() of BaseClass
Let’s consider the same example but in classic ISO/ANSI C++:
class CBaseClass { public: CBaseClass() { Foo(); } virtual void Foo() { cout << "Foo() of CBaseClass" << endl; } }; class CDerivedClass : CBaseClass { public: CDerivedClass() { } virtual void Foo() override { cout << "Foo() of CDerivedClass" << endl; } }; class CChildClass : CDerivedClass { public: CChildClass() { } virtual void Foo() override { cout << "Foo() of CChildClass" << endl; } };
Now, run the code. It should outputs:
Foo() of BaseClass
In classic C++, the overload of the function of the class being constructed is called unlike C# and C++/CLI (.NET in general.)
This article, along with any associated source code and files, is licensed under The Creative Commons Attribution 3.0 Unported License