介紹
如果你是個模板的高手,你就可以將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
我們也可以將數據類型作為一個模板參數來傳遞,並且為它設置 一個默認值。讓我們來稍微修改一下程序37(譯注:原文為“程序 36”,應為37),並將堆棧的尺寸作為一個模板參數來傳遞,而不是作為 構造函數的參數。
程序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一系列功能來分配內存 。如果我們已經確定在創建好堆棧之後就不再更改它的尺寸(就像上面程序中 private段中拷貝構造函數和賦值運算符中的那樣)了,那麼無疑使用模板參數 是更加適合的。
(譯注:作者Amjad在上面兩個程序中並未實現拷貝構造 函數和賦值運算符,這大概是由於這兩者對於本文的內容無關緊要之故吧。在此 我要指出的是正如作者所說,“不是真的要寫一個專業的堆棧類”、 “沒有任何錯誤檢驗”,並且這其中類的組織結構使得精確實現拷貝 構造函數和賦值運算符有一定的難度,尤其是程序37——我們無法從 一個已經定義好的堆棧獲得它的最大容量。)
你也可以將用戶定義的類 作為一個類型參數來傳遞,但是請確認這個類擁有在那個模板函數或類模板中重 載的所有運算符。
例如,請看程序35那個求最大值的函數。這個程序使 用了一個operator >,所以如果我們傳遞自己的類的話,那麼這個類必須重 載了>運算符。下面這個例子示范了這一點。
程序 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>
換為
template <typename T = int, int iSize = 10>
現在我們就沒有必要一定在創建Stack類對 象的時候傳遞數據類型了,但是你仍然需要書寫這一對尖括弧以告知編譯器使用 默認的數據類型。你可以這麼創建對象:
Stack<> st;
當你在類的外部定義(譯注:原文此處是“declare” ,我以為應該是“define”更准確一些。)類模板的成員函數的時候 ,你仍然需要寫出帶有模板參數的類模板全稱。
程序 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,傳遞字符串值(而不是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。(譯注:在我的Visual Studio.net 2003下的輸出卻為Pakistan,這 不同的原因是編譯器組織字符串地址的方式不同決定的,但是Maximum函數的結 果是應該返回內存高位的那個地址的,這和作者說的道理是一致的。)為什麼呢 ?因為這裡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就是使用Strategy設計模式來實現線程的。
代理設計模式也可以使用模板實現,智能指針就是一個例子。下面就是 一個沒有使用模板的簡單版本智能指針。
程序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對象的指針。我們可以編寫Outer類模板來取消這一限制,現在讓 我們來略微修改一下程序。
程序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;
}
程序的輸出和前一個一樣,但是現在 Outer類就可以包含任何類型了,只需要把類型作為模板參數傳遞進來即可。
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組件而創建的類的名稱 。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();
在這裡基類的指針被轉換為派生類的指 針,因為派生類是作為Base類的模板參數傳遞的。這個函數可以通過指針來執行 ,由於指針指向了派生類的對象,所以派生類的對象就被調用了。
但是 這就有一個問題了:我們為什麼要這樣做?答案是:這樣可以節省虛函數帶有的 額外開銷,也就是虛函數表指針、虛函數表以及節省了調用虛函數所花費的額外 時間。這就是ATL中使組件盡可能小、盡可能快的主要思想。
現在,你的 腦海中可能會浮現另外一個問題。如果依靠這一開銷更少的技術可以模擬虛函數 的話,那我們為什麼還要調用虛函數呢?我們不應該用這一技術替換所有的虛函 數嗎?對於這一問題,我可以簡短地回答你:不,我們不能用這一技術替換虛函 數。
其實這一技術還存在一些問題。第一,你不能從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類的指針來存儲派生類的地址的時候。
程序 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類指針的數組存儲不同的派生類,而在虛 函數之中則是可行的。
希望在下一篇文章中能夠探究一些ATL的其它秘密 。