前言:这一系列博客翻译自 The Code Project 上的文章,作者是Zeeshan amjad。
题目:ATL Under the Hood - Part 3
原文链接:http://www.codeproject.com/Articles/2023/ATL-Under-the-Hood-Part-3
介绍
如果你是一个模板编程的高手,那ATL的学习将会轻松许多。这一节我将努力来讲解ATL使用的模板技术。不敢保证你看完这一节后成为模板编程的高手,但我写这一节的目的是,让你在今后看ATL源码时感觉舒服一些。请看如下程序:
程序 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; }
这个程序的输出结果是:
10 B
这里我们有了模板,就不需要为不同的数据类型,如int和char来重载函数。有一点很重要,那就是参数必须有相同的数据类型。但如果我们想传递不同的参数类型,我们就必须告诉编译器我们希望用什么数据类型。
程序 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; }
程序输出如下:
66 B
我们除了定义模板函数,还可以构造模板类。下面简单的stack类就是一个模板类:
程序 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; }
程序中没有任何的检错代码,因为程序本来只是要展示一下模板的使用。
程序输出结果如下:
30 20 10
我们也可以传递给模板参数数据类型然后赋一个缺省的值。程序36稍作修改,我们将stack的大小作为模板的参数传进去,而不是作为构造函数的参数。
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; }
这个程序输出与前面的程序一致,该程序的关键语句如下:
template <typename T, int iSize = 10>
那到底哪种方式更好呢?传递模板参数一般是比传递构造函数参数要快。因为当你作为模板参数传递栈大小时,给定类型的数组将自动创建。而在构造函数中传递参数意味着构造函数必须在运行期间调用malloc 或者new等函数进行内存的分配。如果我们确定在申请了栈之后不改变其大小(就像上述程序中,我们是通过将复制构造函数和赋值操作符设为私有函数来实现这一点),那么相对于构造函数参数,用模板参数是一个更好的选择。
在类型参数的地方,你也可以传递一个自己定义的类,但首先要确保该类重载了所有的模板函数中用到的操作符。
比如,以程序35为例,该程序用了>操作符,如果我们传递了自己定义的类作为模参数,那么,该类必须重载>操作符。下面是个典型的例子:
程序 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; }
该程序输出结果是:
(15, 20)
我们也可以传递模板类作为模板参数。我们将上述的Point改为模板类,然后作为模板参数传递给Stack模板类。
Prog程序ram 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; }
这个程序的输出是:
(15, 20) (5, 10)
这个程序最重要的语句是:
Stack<Point<int> > st;
记住在两个尖括号中间有个空格,否则编译器会作为>>(右移)操作符对待双括号将导致错误。
这个程序还有个功能,我们还可以传递缺省的模板参数类型值。我们将
template <typename T, int iSize = 10>
修改为:
template <typename T = int, int iSize = 10>
现在,我们在创建Stack类对象时可以不指定类型名,但必须写尖括号以便编译器用缺省的数据类型。你将这样来创建一个对象:
Stack<> st;
当你在模板类的外面申明模板的成员函数时,你必须给出完整的模板类名和模板参数。
程序 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; }
程序输出结果为:
(20, 30)
我们对程序35再稍作修改,为其传入string值,而不是int 或者float值。
程序 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; }
输出结果是:Karachi. 因为这里char*作为模板参数。Karachi 存储在了较高字节的内存位置,所以>操作符只是比较了这两个串的内存地址,而不是字符串本身。
如果我们想要通过两个串的长度来比较两个串,而不是通过气地址来比较,那该怎么办呢?
解决方法是,特化模板数据类型为char*。下面就是模板特化的例子:
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; }
模板类也可以用类似的方法来特化:
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; }
该程序输出结果为:
T version A int version 10
ATL有好几个类有类似的特化版本。比如定义在ATLBASE.H中的CComQIPtr类。模板还可以用在不同的设计模式中。比如策略设计模式可以用模板来实现。
程序 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; }
上述程序中,回合1和回合2是代表一个游戏的两个不同回合的类,战略类按照传递给它的模板参数来决定做什么。
该程序输出结果为:
Round1::Play Round2::Play
ATL的线程模型就是用策略设计模式实现的。
代理设计模式也可以用模板来实现。智能智能就是一个代理设计模式的典型案例。下面的例子是一个简化的智能指针,该案例没有使用模板。
程序 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; }
上述程序输出为:
Inner::Fun()
为简单起见,我们只重载了->运算符,但实际的智能指针需要重载很多操作符,比如=,==,!,&,*等。这个智能指针有个问题:该智能指针只能包含指向Inner对象的指针。我们可以通过将OuterClass类改为模板类来除去这个限制。下面是上述程序稍作修改的版本:
程序 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; }
该程序输出结果与前面的版本一致,不过现在OuterClass可以包含任何对象的指针,只要将该对象的类型作为模板参数传给OuterClass即可。
ATL有两个智能指针类:CComPtr和CcomQIPtr。
借助模板,你可以做一些有意思的事情。比如,可以让你的类在不同的情形下拥有不同的父类。
程序 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; }
程序输出结果是:
Base1::Base1 Drive::Drive Base2::Base2 Drive::Drive
上例中Drive类在创建对象时,根据其模板参数的不同,分别继承自不同的基类Base1和Base2。
ATL使用了这个技术。当你用ATL来制作一个COM组件时,CComObject类将继承你的类。因为ATL预先根本不知道你所创建的,用作COM组件的类名字,所以ATL也是用模板的思路实现的这个功能。CComObject类在ATLCOM.h中定义。
我们还可以在模板的帮助下来模拟虚函数。让我们回忆一下虚函数,下面是一个简单的例子:
程序 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; }
输出结果是:
Drive::fun
我们可以借助模板来实现同样的功能。
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; }
上述程序的输出结果与前一个一致,所以,我们可以用模板来模拟虚函数的调用。
这个程序最有趣的一点是:
class Drive : public Base<Drive> {
这句显示我们可以将子类作为模板参数传递给父类。另一处有意思的地方是基类的doSomething
函数
:
T* pT = static_cast<T*>(this); pT->fun();
因为子类以模板参数传递个了父类,所以这里父类的this指针转化为子类指针。然后利用这个指针来执行函数。由于指针已经指向了派生类,所以派生类的函数将被调用。
但我们为什么要这么做呢?答案是,这样做节省了很多用于存放虚指针,虚表的额外字节,也节省了调用虚函数所需要的额外时间开销。这就是ATL使得其组件尽可能小和尽可能快的内部原理。
既然用这种技术可以模拟虚函数调用来减少内存消耗和时间开销,那我们还需要虚函数吗?我们不能全用这种技术来代替虚函数吗?答案是,不可以,我们不能用这种技术代替所有的虚函数。
这个技术有一些问题。首先,你再也无法从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; }
这个程序的输出与前一个一样。而在虚函数的情况下,应该输出:
MostDrive::fun
另外,这个技术还有一个问题,当你用基类指针来存储一个派生类对象的地址时将出现错误。
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; }
这个程序会报错,我们没有为基类传递模板参数。我们对程序稍作改动以便传递模板参数。
程序 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; }
现在程序正常运行,而且结果也是我们想要的:
Drive::fun
但是,当你从该基类继承多个子类是,问题将再次出现。看下面的示例:
程序 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; }
程序在下列语句处报错:
pBase = new Drive2;
因为pBase
是指向 Base<Drive1>
的指针,而不是指向
Base<Drive2>
的指针。简而言之,你不可以用基类的指针来存放不同的继承类的地址。换句话说,你不能创建一个基类的指针数组来存储不同的派生类对象的地址,而利用虚函数你是完全可以的。
本部分完