Overview
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: Contrsuctors are called in descending order
- Rule #2: In C# lexicology, a destructor and a finalizer refer to the same thing
- Rule #3: Destructors are called in ascending order
- Rule #4: Finalizers are a feature of GC-managed objects only
- Rule #5: You cannot determine when finalizers would be called
- Rule #6: C++/CLI differs between destructors and finalizers
- Rule #7: In C++/CLI and classic C++, you can determine when destructors are called
- Rule #8: In C++/CLI, destructors and finalizers are not called together
- Rule #9: Beware of virtual functions in constructors
Rule #1: Constructors are called in descending order
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
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
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();
}
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
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.
Rule #5: You can’t determine when finalizers would be called
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
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
Rule #7: In C++/CLI and C++, you can determine when destructors are called
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
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 functions in constructors
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.)
Filed under:
C#,
C++/CLI,
The Language Tagged:
.NET,
C,
CodeProject,
CPP,
CSharp,
MCPP