Pages

Saturday, October 31, 2009

Polimorphism without virtual functions

I found an interesting place in an old article on CodeProject - WTL for MFC Programmers, Part I - ATL GUI Classes. It is 2003-2005 years, when COM technology, ATL and WTL were in fashion. This place is about the ATL-style templates. In 2003, when I first time used the ATL and WTL, I was told, that such ATL-style does not creae the virtual functions table, so the classes are smaller. And I accepted it as something given, as a rule or a coding standard. This article explains the subject in a so simple and short manner, so even I understood what's going on and why.


I will try to repeat the main idea in this post, in order to be sure that I remember it well now.
#include <iostream>
using namespace std;

template <class T>
class CBaseT
{

public:
void SayHi()
{
T* pT = static_cast<T*>(this);
pT->ClassName();
}

void ClassName()
{
cout << "This is class CBaseT" << endl;
}
};

class CDerive1 : public CBaseT<CDerive1>
{
};

class CDerive2 : public CBaseT<CDerive2>
{
public:
void ClassName()
{
cout << "This is class CDerived2" << endl;
}
};

int main()
{
CDerive1 one;
CDerive2 two;

one.SayHi();
two.SayHi();
}

Firstly, class name CDerive1 and CDerive2 were declared and in the same line already were used for the inheritance list:
class CDerive1 : public CBaseT<CDerive1>
It is legal, because C++ standard allows to use the name immediately after the definition. This trick allowed the second thing for this code - compile-time virtual function:
void ClassName()
This function is not declared as the virtual function, but, in fact, the application screenshot shows that it behaves exactly as the virtual method.
The trick here is
T* pT = static_cast<T*>(this);
in SayHi method of the CBaseT class. It casts type CBaseT to either CDerive1 or CDerive2 depending on which specialization is being invoked. Because template code is generated at compile-time, this cast is safe, because the this object can be only CDerive1 or CDerive2 and nothing else.


If we use the templates in the usual way, we have to check the pointer, and if it is not NULL, we can call the method as it is shown here:
#include <iostream>
using namespace std;

template <class T>
class CTestT
{
public:
void SayHi(T* pT)
{
if (pT != NULL)
pT->ClassName();
}

};

class CTest
{
public:
void ClassName()
{
cout << "This is CTest" << endl;
}
};

int main()
{
CTest test;
CTestT<CTest> one_more_test;
one_more_test.SayHi(&test);
return 0;
}
Check for NULL in the ATL-style templates simply does not exist - it is the this pointer.
The trouble will happen, if I will write:
class CDerive2 : public CBaseT<CDerive1>
So the benefits are obvious:
  1. It does not require using pointers to objects.
  2. It saves memory because there is no need for the virtual functions' table.
  3. It's impossible to call a virtual function through a NULL pointer at run-time because of unintialized vtbl.
  4. All function calls are resolved at compile-time, so they can be optimized.
If you remember the classical "Effective C++" (Scott Meyers), such use of the templates as it was shown above is a trick, it is a little bit against Scott Meyers formula:
  • A template should be used to generate a collection of classes when the type of the objects does not affect the behavior of the class's functions.
  • Inheritance should be used for a collection of classes when the type of the objects does affect the behavior of the class's functions.

No comments:

Post a Comment