Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ATL Under the Hood Part 3

0.00/5 (No votes)
26 Mar 2002 1  
Contininuing the ATL Under the Hood series to explain the inner workings of ATL

Introduction

One can enjoy the study of ATL if he/she is black belt in Templates. In this episode I will try to explain some of the template techniques which ATL uses. I don't guarantee that you will become a black belt in templates after reading this episode, but I am trying my best that you will feel more comfortable with ATL source code after reading this.

Program 35

#include <iostream>

using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
	return a > b ? a : b;
}
int main() {
	cout << Maximum(5, 10) << endl;
	cout << Maximum('A', 'B') << endl;

	return 0;
}

The output of this program is

10
B

Here we don't need to overload the function for int and char data type due to template function. Here one thing is important that both parameter of the function should have the same data type. But if we want to pass different data type then we have to tell the compiler that which data type will be considered.

Program 36

#include <iostream>

using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
	return a > b ? a : b;
}

int main() {
	cout << Maximum<int>(5, 'B') << endl;
	cout << Maximum<char>(5, 'B') << endl;

	return 0;
}

The output of this program is

66
B

We can make the class template too. Here is simplified version of template stack class.

Program 37

#include <iostream>

using namespace std;

template <typename T>
class Stack {
private:
	T* m_pData;
	int m_iTop;

public:
	Stack(int p_iSize = 0) : m_iTop(0) {
		m_pData = new T[p_iSize];
	}

	void Push(T p_iData) {
		m_pData[m_iTop++] = p_iData;
	}

	T Pop() {
		return m_pData[--m_iTop];
	}

	T Top() {
		return m_pData[m_iTop];
	}

	~Stack() {
		if (m_pData) {
			delete [] m_pData;
		}
	}

private:
	Stack(const Stack<T>&);
	Stack<T>& operator = (const Stack<T>&);
};

int main() {
	Stack<int> a(10);

	a.Push(10);
	a.Push(20);
	a.Push(30);

	cout << a.Pop() << endl;
	cout << a.Pop() << endl;
	cout << a.Pop() << endl;
	
	return 0;
}

There isn't any robust error checking in this program, but the purpose of this program is to show the usage of template not making a stack class used professionally.

The output of this program is

30
20
10

We can also pass the data type as a template argument and assign a default value to it. Let's change program 36 little bit and pass the size of stack as a template parameter rather than constructor parameter.

Program 38

#include <iostream>

using namespace std;

template <typename T, int iSize = 10>
class Stack {
private:
	T m_pData[iSize];
	int m_iTop;

public:
	Stack() : m_iTop(0) {
	}

	void Push(T p_iData) {
		m_pData[m_iTop++] = p_iData;
	}

	T Pop() {
		return m_pData[--m_iTop];
	}

	T Top() {
		return m_pData[m_iTop];
	}

private:
	Stack(const Stack<T>&);
	Stack<T>& operator = (const Stack<T>&);
};

int main() {
	Stack<int, 10> a;

	a.Push(10);
	a.Push(20);
	a.Push(30);

	cout << a.Pop() << endl;
	cout << a.Pop() << endl;
	cout << a.Pop() << endl;
	
	return 0;
}

The output of this program is same as pervious program. The important part of this program is.

template <typename T, int iSize = 10>

Now there is a question which approach is better? Passing template parameter is always faster then passing parameter by constructor. Why? Because when you pass stack size in template parameter then the array of given data type is created automatic (i.e. created on stack), however passing parameter by constructor means constructor has to allocate memory during run time by new or malloc family of functions. If we are sure that we are not going to change the size of stack once we made this, as we have done in above program by making copy constructor and assignment operator private, then use template parameter is better approach.

You can also pass user define class as a parameter in place of type parameter, but be sure that class have all the overloaded operators which is used in that template function or template class.

E.g. take a look at Maximum function of program 35. This program use one operator >, so if we pass our own class then that class must have overloaded > operator. Here is a program which shows this.

Program 39

#include <iostream>

using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
	return a > b ? a : b;
}

class Point {
private:
	int m_x, m_y;

public:
	Point(int p_x = 0, int p_y = 0) : m_x(p_x), m_y(p_y) {
	}

	bool friend operator > (const Point& lhs, const Point& rhs) {
		return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
	}

	friend ostream& operator << (ostream& os, const Point& p) {
		return os << "(" << p.m_x << ", " << p.m_y  << ")";
	}
};

int main() {
	Point a(5, 10), b(15, 20);
	cout << Maximum(a, b) << endl;

	return 0;
}

The output of this program is

(15, 20)

We can also pass the template class as a template parameter. Let's make this Point class template and pass it as a template parameter to Stack template class.

Program 40

#include <iostream>

using namespace std;

template <typename T>
class Point {
private:
	T m_x, m_y;

public:
	Point(T p_x = 0, T p_y = 0) : m_x(p_x), m_y(p_y) {
	}

	bool friend operator > (const Point<T>& lhs, const Point<T>& rhs) {
		return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
	}

	friend ostream& operator << (ostream& os, const Point<T>& p) {
		return os << "(" << p.m_x << ", " << p.m_y  << ")";
	}
};

template <typename T, int iSize = 10>
class Stack {
private:
	T m_pData[iSize];
	int m_iTop;

public:
	Stack() : m_iTop(0) {
	}

	void Push(T p_iData) {
		m_pData[m_iTop++] = p_iData;
	}

	T Pop() {
		return m_pData[--m_iTop];
	}

	T Top() {
		return m_pData[m_iTop];
	}

private:
	Stack(const Stack<T>&);
	Stack<T>& operator = (const Stack<T>&);
};

int main() {
	Stack<Point<int> > st;

	st.Push(Point<int>(5, 10));
	st.Push(Point<int>(15, 20));

	cout << st.Pop() << endl;
	cout << st.Pop() << endl;

	return 0;
}

The output of this program is

(15, 20)
(5, 10)

The most important part of this program is

Stack<Point<int> > st;

Here you have to pass the space between the two angle brackets, other wise compiler treat it >> (shift right operator) and generate error.

There is one more thing we can do with this program. We can pass the default type value of template parameter too. We can change

template <typename T, int iSize = 10>
to
template <typename T = int, int iSize = 10>

Now we don't have to pass the data type at the time of creating the object from Stack class. But you have to write the blank angle brackets at the time of creating the object to order the compiler to use default data type. You will create object something like this.

Stack<> st;

When you declare template class member function outside the class then you have to give the complete name of template class with its Template parameter.

Program 41

#include <iostream>

using namespace std;

template <typename T>
class Point {
private:
	T m_x, m_y;

public:
	Point(T p_x = 0, T p_y = 0);
	void Setxy(T p_x, T p_y);
	T getX() const;
	T getY() const;

	friend ostream& operator << (ostream& os, const Point<T>& p) {
		return os << "(" << p.m_x << ", " << p.m_y  << ")";
	}
};

template <typename T>
Point<T>::Point(T p_x, T p_y) : m_x(p_x), m_y(p_y) {
}

template <typename T>
void Point<T>::Setxy(T p_x, T p_y) {
	m_x = p_x;
	m_y = p_y;
}

template <typename T>
T Point<T>::getX() const {
	return m_x;
}

template <typename T>
T Point<T>::getY() const {
	return m_y;
}

int main() {
	Point<int> p;
	p.Setxy(20, 30);
	cout << p << endl;

	return 0;
}

The output of the program is

(20, 30)

Lets change program 35 little bit and pass string value rather than int or float and see the result.

Program 42

#include <iostream>

using namespace std;

template <typename T>
T Maximum(T a, T b) {
	return a > b ? a : b;
}

int main() {
	cout << Maximum("Pakistan", "Karachi") << endl;
	
	return 0;
}

The output of this program is Karachi. Why? Because here char* is passed as a template parameter. Karachi is stored at higher memory location so the > operator just compare the value of address rather then the string itself.

What should we do if we want the comparison on the basis of length of string not on the basis of their address?

The solution is to specialized the template on char* data type. Here is an example of template specialization.

Program 43

#include <iostream>

using namespace std;

template <typename T>
T Maximum(T a, T b) {
	return a > b ? a : b;
}

template <>
char* Maximum(char* a, char* b) {
	return strlen(a) > strlen(b) ? a : b;
}

int main() {
	cout << Maximum("Pakistan", "Karachi") << endl;
	
	return 0;
}

Classes can be specialized in the same way.

.

Program 44

#include <iostream>

using namespace std;

template <typename T>
class TestClass {
public:
	void F(T pT) {
		cout << "T version" << '\t';
		cout << pT << endl;
	}
};

template <>
class TestClass<int> {
public:
	void F(int pT) {
		cout << "int version" << '\t';
		cout << pT << endl;
	}
};

int main() {
	TestClass<char> obj1;
	TestClass<int> obj2;

	obj1.F('A');
	obj2.F(10);

	return 0;
}

The output of this program is

T version	A
int version	10

ATL has several classes which have specialized version like this, such as CComQIPtr define in ATLBASE.H

Template can also be used in different design patter. E.g. Strategy design pattern can be implemented by using template.

Program 45

#include <iostream>

using namespace std;

class Round1 {
public:
	void Play() {
		cout << "Round1::Play" << endl;
	}
};

class Round2 {
public:
	void Play() {
		cout << "Round2::Play" << endl;
	}
};

template <typename T>
class Strategy {
private:
	T objT;
public:
	void Play() {
		objT.Play();
	}
};

int main() {
	Strategy<Round1> obj1;
	Strategy<Round2> obj2;

	obj1.Play();
	obj2.Play();

	return 0;
}

Here Round1 and Round2 are classes of different round of a game and Strategy class decide what do to do depend on the template parameter pass to this class.

The output of the program is

Round1::Play
Round2::Play

ATL implement threading using Strategy design pattern.

Proxy design pattern can also be implemented using template. Smart pointer is an example of proxy design pattern. Here is an example of simplified version of smart pointer without using template.

Program 46

#include <iostream>

using namespace std;

class Inner {
public:
	void Fun() {
		cout << "Inner::Fun" << endl;
	}
};

class Outer {
private:
	Inner* m_pInner;

public:
	Outer(Inner* p_pInner) : m_pInner(p_pInner) {
	}

	Inner* operator -> () {
		return m_pInner;
	}
};

int main() {
	Inner objInner;
	Outer objOuter(&objInner);

	objOuter->Fun();

	return 0;
}

The output of the program is

Inner::Fun()

For simplicity, we just overload the -> operator, but in real smart pointer all the necessary operators such as =, ==, !, &, * are overloaded. There is one big problem with this smart pointer; this can only contain pointer to Inner object. We can remove this restriction by making OuterClass template. Let's change program little bit.

Program 47

#include <iostream>

using namespace std;

class Inner {
public:
	void Fun() {
		cout << "Inner::Fun" << endl;
	}
};

template <typename T>
class Outer {
private:
	T* m_pInner;

public:
	Outer(T* p_pInner) : m_pInner(p_pInner) {
	}

	T* operator -> () {
		return m_pInner;
	}
};

int main() {
	Inner objInner;
	Outer<Inner> objOuter(&objInner);

	objOuter->Fun();

	return 0;
}

The output of the program is same as previous one but now OuterClass can contain any class whose type is passed as a template parameter.

ATL has two smart pointer classes. CComPtr and CComQIPtr.

You can do some interesting work with the help of template. E.g. your class can be child of different base class depend on different situation.

Program 48

#include <iostream>

using namespace std;

class Base1 {
public:
	Base1() {
		cout << "Base1::Base1" << endl;
	}
};

class Base2 {
public:
	Base2() {
		cout << "Base2::Base2" << endl;
	}
};

template <typename T>
class Drive : public T {
public:
	Drive() {
		cout << "Drive::Drive" << endl;
	}
};

int main() {

	Drive<Base1> obj1;
	Drive<Base2> obj2;
	return 0;
}

The output of this program is

Base1::Base1
Drive::Drive
Base2::Base2
Drive::Drive

Here the Drive class is inherited from Base1 and Base2 depend on the parameter passed to the template at the time of creation of object.

ATL use this technique. When you make a COM component using ATL then CComObject is inherit from your class. Here ATL take advantage of template, because ATL doesn't know in advance the name of the class which you create to make COM component. CComObject class is define in ATLCOM.h file

We can simulate virtual functions too with the help of template. Let's recall virtual function once again. Here is a simple program to recall the virtual function.

Program 49

#include <iostream>

using namespace std;

class Base {
public:
	virtual void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		fun();
	}
};

class Drive : public Base {
public:
	void fun() {
		cout << "Drive::fun" << endl;
	}
};

int main() {
	Drive obj;
	obj.doSomething();

	return 0;
}

The output of the program is

Drive::fun

We can get the same behavior with the help of template.

Program 50

#include <iostream>

using namespace std;

template <typename T>
class Base {
public:
	void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		T* pT = static_cast<T*>(this);
		pT->fun();
	}
};

class Drive : public Base<Drive> {
public:
	void fun() {
		cout << "Drive::fun" << endl;
	}
};

int main() {
	Drive obj;
	obj.doSomething();

	return 0;
}

The output of the program is same as previous one. So we can simulate the behavior of virtual function with the help of template.

The interesting parts of this program are

class Drive : public Base<Drive> {

This shows that we can pass the Drive class as a template parameter. The other interesting part of the program is the doSomething function of base class.

T* pT = static_cast<T*>(this);
pT->fun();

Here the base class pointer is converted into drive class pointer, because drive class is passed as a template parameter of Base class. Then function is executed from that pointer, Now that pointer points to drive class object, so the drive class object is called.

But there is very good question, why should we do this? And there is very good answer of this question so save the extra byes of virtual pointer, virtual table and save the extra time to call virtual function. This is the main philosophy of ATL to make component as small as possible and as fast as possible.

Now there is one more question may arise in the mind. If due to this technique you can simulate virtual function with less memory and faster then original virtual function then why should we call virtual function? Shouldn't we replace all virtual function with this technique? The short answer of this question is no, we can not replace all virtual function with this technique.

There are some problems with this technique. First you can not further inherit any class from Drive class. If you try to do so then that function will no more act as a virtual function. It doesn't happen in the case of virtual function; once you declare function virtual then it becomes virtual in all of drive class no matter how deep in inheritance chain. Let's take a look at a program what happen when we inherit one more class from Drive.

Program 51

#include <iostream>

using namespace std;

template <typename T>
class Base {
public:
	void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		T* pT = static_cast<T*>(this);
		pT->fun();
	}
};

class Drive : public Base<Drive> {
public:
	void fun() {
		cout << "Drive::fun" << endl;
	}
};

class MostDrive : public Drive {
public:
	void fun() {
		cout << "MostDrive::fun" << endl;
	}
};

int main() {
	MostDrive obj;
	obj.doSomething();

	return 0;
}

The output of this is program as same as previous one. In case of virtual function the output should be

MostDrive::fun

There is one more problem with this technique, when we want to take pointer of Base class and want to store address of drive class.

Program 52

#include <iostream>

using namespace std;

template <typename T>
class Base {
public:
	void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		T* pT = static_cast<T*>(this);
		pT->fun();
	}
};

class Drive : public Base<Drive> {
public:
	void fun() {
		cout << "Drive::fun" << endl;
	}
};

int main() {
	Base* pBase = NULL;
	pBase = new Drive;

	return 0;
}

This program gives an error, because we couldn't pass the template parameter of the base class. Now change program little bit and pass the template parameter.

Program 53

#include <iostream>

using namespace std;

template <typename T>
class Base {
public:
	void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		T* pT = static_cast<T*>(this);
		pT->fun();
	}
};

class Drive : public Base<Drive> {
public:
	void fun() {
		cout << "Drive::fun" << endl;
	}
};

int main() {
	Base<Drive>* pBase = NULL;
	pBase = new Drive;
	pBase->doSomething();

	return 0;
}

Now this program works fine and gives the same output as we expected i.e.

Drive::fun

But there is a problem when you inherit more then one class from Base class. To better understand it, take a look at the following program.

Program 54

#include <iostream>

using namespace std;

template <typename T>
class Base {
public:
	void fun() {
		cout << "Base::fun" << endl;
	}

	void doSomething() {
		T* pT = static_cast<T*>(this);
		pT->fun();
	}
};

class Drive1 : public Base<Drive1> {
public:
	void fun() {
		cout << "Drive1::fun" << endl;
	}
};

class Drive2 : public Base<Drive2> {
public:
	void fun() {
		cout << "Drive2::fun" << endl;
	}
};

int main() {
	Base<Drive1>* pBase = NULL;
	pBase = new Drive1;
	pBase->doSomething();
	delete pBase;

	pBase = new Drive2;
	pBase->doSomething();

	return 0;
}

This program gives error at

pBase = new Drive2;

Because pBase is a pointer to Base<Drive1> not Base<Drive2>. In short you can't make pointer of Base class and store address of different Drive class in it. In other words you cant make an array of Base pointer and store address of different drive class in it, which you can do in case of virtual function.

Hope to explore some other mysterious of ATL in next article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here