ATL 内部机制 第三部分






4.88/5 (20投票s)
继续 ATL 内部机制系列,
引言
如果一个人在模板方面是黑带高手,那么他可以享受学习 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
我们也可以创建类模板。这是一个模板栈类的简化版本。
程序 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,将栈的大小作为模板参数而不是构造函数参数来传递。
程序 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>
现在有一个问题,哪种方法更好?通过模板参数传递总是比通过构造函数传递参数快。为什么?因为当你将栈大小作为模板参数传递时,相应数据类型的数组会被自动创建(即在栈上创建),而通过构造函数传递参数意味着构造函数需要在运行时通过 new 或 malloc 系列函数分配内存。如果我们确信在创建栈后不会改变它的大小,就像我们在上面通过将复制构造函数和赋值运算符设为私有所做的那样,那么使用模板参数是更好的方法。
你也可以将用户定义的类作为参数传递,而不是类型参数,但要确保该类拥有模板函数或模板类中使用的所有重载运算符。
例如,看看程序 35 的 Maximum 函数。这个程序使用了一个 > 运算符,所以如果我们传递我们自己的类,那么该类必须重载 > 运算符。下面是一个展示此点的程序。
程序 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 模板类。
程序 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>to
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* 数据类型进行模板特化。下面是一个模板特化的例子。
程序 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; }
类也可以以同样的方式进行特化。
.程序 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; }
这里 Round1 和 Round2 是游戏中不同轮次的类,Strategy 类根据传递给该类的模板参数决定做什么。
程序输出是:
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 可以包含任何类型为模板参数的类。
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 利用了模板的优势,因为 ATL 无法预先知道你创建 COM 组件的类的名称。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
我们可以通过模板获得相同的行为。
程序 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> {
这表明我们可以将 Drive 类作为模板参数传递。程序中另一个有趣的部分是基类的 doSomething
函数。
T* pT = static_cast<T*>(this); pT->fun();
这里基类指针被转换为 Drive 类指针,因为 Drive 类被传递为 Base 类的模板参数。然后通过该指针执行函数。现在该指针指向 Drive 类对象,因此调用的是 Drive 类对象。
但是有一个很好的问题,我们为什么要这样做?有一个很好的答案:节省虚指针、虚表的额外开销,并节省调用虚函数的额外时间。这是 ATL 的主要理念,即使组件尽可能小,尽可能快。
现在可能会产生另一个问题。如果由于这项技术,你可以用更少的内存和更快的速度模拟虚函数,而不是原始的虚函数,那么我们为什么要调用虚函数呢?我们难道不应该用这项技术替换所有虚函数吗?这个问题的简短答案是:不,我们不能用这项技术替换所有虚函数。
这项技术存在一些问题。首先,你不能从 Drive
类进一步继承任何类。如果你尝试这样做,那么该函数将不再像虚函数一样工作。这在虚函数的情况下不会发生;一旦你声明一个函数为虚函数,它在所有 Drive 类中都会是虚函数,无论继承链有多深。让我们来看一个程序,看看当我们从 Drive 继承另一个类时会发生什么。
程序 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
这项技术还有另一个问题,当我们想获取 Base
类的指针并想存储 Drive 类的地址时。
程序 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
但是,当我们从 Base
类继承多个类时,会有一个问题。为了更好地理解它,请看以下程序。
程序 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>
的指针。简而言之,你不能创建 Base
类的指针并存储不同 Drive 类的地址。换句话说,你不能创建一个 Base 指针数组并存储不同 Drive 类的地址,而这在你使用虚函数时是可以做到的。
希望在下一篇文章中能探索 ATL 的更多神秘之处。