神奇的C++模版
介紹
如果你是個(gè)模板的高手,你就可以將ATL的學(xué)習(xí)作為一種享受。在這一節(jié)中,我將要嘗試解釋一些ATL使用的模板技術(shù)。我不能保證你讀完本節(jié)后能成為一個(gè)模板高手,只能是盡我所能讓你在讀完本文后能夠更輕松地理解ATL的源碼。
程序35.
程序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 我們也可以編寫(xiě)類模板,下面就是一個(gè)簡(jiǎn)單版本的堆棧類模板。
程序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; }這個(gè)程序中沒(méi)有任何錯(cuò)誤檢驗(yàn),不過(guò)這個(gè)程序的目的只是示范模板的用法,而不是真的要寫(xiě)一個(gè)專業(yè)的堆棧類。
程序的輸出為:30 20 10我們也可以將數(shù)據(jù)類型作為一個(gè)模板參數(shù)來(lái)傳遞,并且為它設(shè)置一個(gè)默認(rèn)值。讓我們來(lái)稍微修改一下程序37(譯注:原文為“程序36”,應(yīng)為37),并將堆棧的尺寸作為一個(gè)模板參數(shù)來(lái)傳遞,而不是作為構(gòu)造函數(shù)的參數(shù)。
程序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; } 程序的輸出和前一個(gè)相同。這個(gè)程序最重要的一點(diǎn)為:template <typename T, int iSize = 10>現(xiàn)在就有一個(gè)問(wèn)題:哪一個(gè)更好呢?通常,傳遞模板參數(shù)的辦法是優(yōu)于給構(gòu)造函數(shù)傳遞參數(shù)的。為什么呢?因?yàn)樵谀銓⒍褩3叽缱鳛槟0鍏?shù)傳遞的時(shí)候,這個(gè)給定數(shù)據(jù)類型的數(shù)組就會(huì)被自動(dòng)創(chuàng)建;而給構(gòu)造函數(shù)傳遞參數(shù)則意味著構(gòu)造函數(shù)會(huì)在運(yùn)行時(shí)使用new或malloc一系列功能來(lái)分配內(nèi)存。如果我們已經(jīng)確定在創(chuàng)建好堆棧之后就不再更改它的尺寸(就像上面程序中private段中拷貝構(gòu)造函數(shù)和賦值運(yùn)算符中的那樣)了,那么無(wú)疑使用模板參數(shù)是更加適合的。
(譯注:作者Amjad在上面兩個(gè)程序中并未實(shí)現(xiàn)拷貝構(gòu)造函數(shù)和賦值運(yùn)算符,這大概是由于這兩者對(duì)于本文的內(nèi)容無(wú)關(guān)緊要之故吧。在此我要指出的是正如作者所說(shuō),“不是真的要寫(xiě)一個(gè)專業(yè)的堆棧類”、“沒(méi)有任何錯(cuò)誤檢驗(yàn)”,并且這其中類的組織結(jié)構(gòu)使得精確實(shí)現(xiàn)拷貝構(gòu)造函數(shù)和賦值運(yùn)算符有一定的難度,尤其是程序37——我們無(wú)法從一個(gè)已經(jīng)定義好的堆棧獲得它的最大容量。)
你也可以將用戶定義的類作為一個(gè)類型參數(shù)來(lái)傳遞,但是請(qǐng)確認(rèn)這個(gè)類擁有在那個(gè)模板函數(shù)或類模板中重載的所有運(yùn)算符。
例如,請(qǐng)看程序35那個(gè)求最大值的函數(shù)。這個(gè)程序使用了一個(gè)operator >,所以如果我們傳遞自己的類的話,那么這個(gè)類必須重載了>運(yùn)算符。下面這個(gè)例子示范了這一點(diǎn)。
程序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)同樣,我們也能夠?qū)⒁粋€(gè)類模板作為一個(gè)模板參數(shù)傳遞。現(xiàn)在讓我們來(lái)編寫(xiě)這樣一個(gè)Point類,并將其作為一個(gè)模板參數(shù)傳遞給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)這個(gè)程序中最重要的部分為:Stack<Point<int> > st;在這里,你必須在兩個(gè)大于號(hào)之間放置一個(gè)空格,否則編譯器就會(huì)將它看作>>(右移運(yùn)算符)并產(chǎn)生錯(cuò)誤。
對(duì)于這個(gè)程序我們還可以這么做,就是為模板參數(shù)傳遞默認(rèn)的類型值,也就是將template <typename T, int iSize = 10>換為template <typename T = int, int iSize = 10>現(xiàn)在我們就沒(méi)有必要一定在創(chuàng)建Stack類對(duì)象的時(shí)候傳遞數(shù)據(jù)類型了,但是你仍然需要書(shū)寫(xiě)這一對(duì)尖括弧以告知編譯器使用默認(rèn)的數(shù)據(jù)類型。 你可以這么創(chuàng)建對(duì)象:Stack<> st;當(dāng)你在類的外部定義(譯注:原文此處是“declare”,我以為應(yīng)該是“define”更準(zhǔn)確一些。)類模板的成員函數(shù)的時(shí)候,你仍然需要寫(xiě)出帶有模板參數(shù)的類模板全稱。
程序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)讓我們來(lái)稍微修改一下程序35,傳遞字符串值(而不是int或float)作為參數(shù),并看看結(jié)果吧。
程序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函數(shù)的結(jié)果是應(yīng)該返回內(nèi)存高位的那個(gè)地址的,這和作者說(shuō)的道理是一致的。)為什么呢?因?yàn)檫@里char*作為模板參數(shù)傳遞, Karachi在內(nèi)存中存儲(chǔ)的位置更高,而>運(yùn)算符僅僅比較這兩個(gè)地址值而不是字符串本身。
那么,如果我們希望基于字符串的長(zhǎng)度來(lái)比較而不是地址的話,應(yīng)該怎么做呢?
解決的辦法是對(duì)char*數(shù)據(jù)類型進(jìn)行模板的特化。下面是一個(gè)模板特化的例子。
程序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; }至于類模板,也可以用相同的辦法進(jìn)行特化。
程序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 10ATL中就有若干類是類似這樣的特化版本,例如在ATLBASE.H中定義的CComQIPtr。
模板也可以在不同的設(shè)計(jì)模式中使用,例如策略設(shè)計(jì)模式可以使用模板實(shí)現(xiàn)。
程序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為一個(gè)游戲中不同的關(guān)卡類,并且Strategy類依靠傳遞的模板參數(shù)來(lái)決定該做些什么。
程序的輸出為:Round1::Play Round2::PlayATL就是使用Strategy設(shè)計(jì)模式來(lái)實(shí)現(xiàn)線程的。
代理設(shè)計(jì)模式也可以使用模板實(shí)現(xiàn),智能指針就是一個(gè)例子。下面就是一個(gè)沒(méi)有使用模板的簡(jiǎn)單版本智能指針。
程序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()簡(jiǎn)單地說(shuō)來(lái),我們僅僅重載了->運(yùn)算符,但是在實(shí)際的智能指針中,所有必須的運(yùn)算符(例如=、==、!、&、*)都需要被重載。以上的智能指針有一個(gè)大問(wèn)題:它只能包含指向Inner對(duì)象的指針。我們可以編寫(xiě)Outer類模板來(lái)取消這一限制,現(xiàn)在讓我們來(lái)略微修改一下程序。
程序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; }程序的輸出和前一個(gè)一樣,但是現(xiàn)在Outer類就可以包含任何類型了,只需要把類型作為模板參數(shù)傳遞進(jìn)來(lái)即可。
ATL中有兩個(gè)智能指針,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是由在對(duì)象創(chuàng)建的時(shí)候傳遞給模板的參數(shù)決定的。
ATL也使用了這一技術(shù)。當(dāng)你使用ATL創(chuàng)建COM組件的時(shí)候,CComObject就會(huì)繼承自你的類。在這里ATL利用了模板,因?yàn)樗粫?huì)預(yù)先知道你用來(lái)作COM組件而創(chuàng)建的類的名稱。CComObject類定義于ATLCOM.H文件之中。
在模板的幫助下,我們也可以模擬虛函數(shù)。現(xiàn)在讓我們重新回憶一下虛函數(shù),下面是一個(gè)簡(jiǎn)單的例子。
程序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在模板的幫助下,我們可以實(shí)現(xiàn)與之相同的行為。
程序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; }程序的輸出和前一個(gè)是一樣的,所以我們可以用模板來(lái)模擬虛函數(shù)的行為。
程序中一個(gè)有趣的地方為class Drive : public Base<Drive> {這表明我們可以將Drive類作為一個(gè)模板參數(shù)來(lái)傳遞。程序中另外一個(gè)有趣的地方是基類中的doSomething函數(shù)。T* pT = static_cast<T*>(this); pT->fun();在這里基類的指針被轉(zhuǎn)換為派生類的指針,因?yàn)榕缮愂亲鳛锽ase類的模板參數(shù)傳遞的。這個(gè)函數(shù)可以通過(guò)指針來(lái)執(zhí)行,由于指針指向了派生類的對(duì)象,所以派生類的對(duì)象就被調(diào)用了。
但是這就有一個(gè)問(wèn)題了:我們?yōu)槭裁匆@樣做?答案是:這樣可以節(jié)省虛函數(shù)帶有的額外開(kāi)銷(xiāo),也就是虛函數(shù)表指針、虛函數(shù)表以及節(jié)省了調(diào)用虛函數(shù)所花費(fèi)的額外時(shí)間。這就是ATL中使組件盡可能小、盡可能快的主要思想。
現(xiàn)在,你的腦海中可能會(huì)浮現(xiàn)另外一個(gè)問(wèn)題。如果依靠這一開(kāi)銷(xiāo)更少的技術(shù)可以模擬虛函數(shù)的話,那我們?yōu)槭裁催€要調(diào)用虛函數(shù)呢?我們不應(yīng)該用這一技術(shù)替換所有的虛函數(shù)嗎?對(duì)于這一問(wèn)題,我可以簡(jiǎn)短地回答你:不,我們不能用這一技術(shù)替換虛函數(shù)。
其實(shí)這一技術(shù)還存在一些問(wèn)題。第一,你不能從Drive類進(jìn)行更深層的繼承,如果你試著這么做,那么它將不再會(huì)是虛函數(shù)的行為了。而對(duì)于虛函數(shù)來(lái)說(shuō),這一切就不會(huì)發(fā)生。一旦你將函數(shù)聲明為虛函數(shù),那么在派生類中的所有函數(shù)都會(huì)成為虛函數(shù),無(wú)論繼承鏈有多深。現(xiàn)在我們看看當(dāng)從Drive中再繼承一個(gè)類的時(shí)候會(huì)發(fā)生什么。
程序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; }程序的輸出和前一個(gè)一樣。但是對(duì)于虛函數(shù)的情況來(lái)說(shuō),輸出就應(yīng)該是:MostDrive::fun這一技術(shù)還有另外一個(gè)問(wèn)題,就是當(dāng)我們使用Base類的指針來(lái)存儲(chǔ)派生類的地址的時(shí)候。
程序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; }這個(gè)程序會(huì)給出一個(gè)錯(cuò)誤,因?yàn)槲覀儧](méi)有向基類傳遞模板參數(shù)。現(xiàn)在我們稍微修改一下,并傳遞模板參數(shù)。
程序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; }現(xiàn)在程序正常工作,并給出了我們所期望的輸出,也就是:Drive::fun但是在Base類有多個(gè)繼承的時(shí)候,就會(huì)出現(xiàn)問(wèn)題。為了更好地弄懂這一點(diǎn),請(qǐng)看下面的程序。
程序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; }程序會(huì)在下面的代碼處給出錯(cuò)誤:pBase = new Drive2;
因?yàn)閜Base是一個(gè)指向Base<Drive1>的指針,而不是Base<Drive2>。簡(jiǎn)單地說(shuō)來(lái),就是你不能使Base類的指針指向不同的Drive類。換句話說(shuō),你不能使用Base類指針的數(shù)組存儲(chǔ)不同的派生類,而在虛函數(shù)之中則是可行的。
原文:http://www.vckbase.com/document/viewdoc/?id=1352
總結(jié)
- 上一篇: VIM常用命令集合
- 下一篇: [整理I]精选微软等公司数据结构+算法面