# [35] 模板 ?
## FAQs in section [35]:
* [35.1] 模板的設計思想是什么?
* [35.2] 什么是 “類模板”的語法/語義?
* [35.3] 什么是“函數模板”的語法/語義?
* [35.4] 如何確定顯式調用函數模板的哪個版本?
* [35.5] 什么是“參數化類型”?
* [35.6] 什么是“泛型”?
* [35.7] 當模板類型T是 int 或std::string時,我的模板函數需要進行特殊處理。對特殊類型的T我該怎么實現模板特化?
* [35.8] 哈?你能提供一個具體的模板特化的例子嗎??
* [35.9] 但是模板函數的大部分代碼是相同的,是否有辦法實現模板特化并且不用重復復制所有的源代碼?
* [35.10] 所有這些模板和模板特化都會降低程序執行速度,對不對?
* [35.11] 因此 模板重載了函數,對不對?
* [35.12] 為什么不能分開模板的聲明和定義,把定義放到.cpp文件中?
* [35.13] 如何避免模板函數的鏈接錯誤?
* [35.14] 如何使用C++的關鍵字export來避免模板鏈接錯誤??
* [35.15] 如何避免模板類的鏈接錯誤?
* [35.16] 為什么我收到鏈接錯誤 ,當我使用模板友元的時候?
* [35.17] 怎么理解這些繁瑣的模板錯誤信息?
* [35.18]當模板派生類使用一個繼承自模板基類的嵌套類型時,為什么出錯??
* [35.19]當模板派生類使用使用一個繼承自模板基類的成員變量時 ,為什么出錯??
* [35.20] 前一個問題可以暗傷我?難道編譯器默認地產生錯誤代碼??
## 35.1 模板的設計思想是什么?
模板像是甜餅切割器,指定如何切割cookies讓他們看起來大致相同(雖然Cookie由各種面團來制作,但是他們都會有相同的基本形狀)。同樣,類模板是描述如何建立一個類族,讓所有的類看起來是基本相同;函數模板描述如何建立一個外觀類似的函數族。
類模板通常用于構建類型安全的容器(although this only scratches the surface for how they can be used)。
## 35.2 什么是 “類模板”的語法/語義?
考慮一個容器`類class Array`,它的行為像一個整數數組:
```
//?This?would?go?into?a?header?file?such?as?"__Array.h__"
?class?Array?{
?public:
???Array(int?len=10)??????????????????:?len_(len),?data_(new?int[len])?{?}
??~Array()????????????????????????????{?delete[]?data_;?}
???int?len()?const????????????????????{?return?len_;?????}
???const?int&?operator[](int?i)?const?{?return?data_[check(i)];?}??←?subscript?operators?often?come?in?pairs What's the deal with "const-overloading"?")
?????????int&?operator[](int?i)???????{?return?data_[check(i)];?}??←?subscript?operators?often?come?in?pairs What's the deal with "const-overloading"?")
???Array(const?Array&);
???Array&?operator=?(const?Array&);
?private:
???int??len_;
???int*?data_;
???int??check(int?i)?const
?????{?if?(i?<?0?||?i?>=?len_)?throw?BoundsViol("Array",?i,?len_);
???????return?i;?}
?};
```
對于浮點數數組,字符數組,`std::string`數組,`std::string`數組的數組等,反復重復上述步驟將很冗長乏味。
```
//?This?would?go?into?a?header?file?such?as?"__Array.h__"
?template<typename?T>
?class?Array?{
?public:
???Array(int?len=10)????????????????:?len_(len),?data_(new?T[len])?{?}
??~Array()??????????????????????????{?delete[]?data_;?}
???int?len()?const??????????????????{?return?len_;?????}
???const?T&?operator[](int?i)?const?{?return?data_[check(i)];?}
?????????T&?operator[](int?i)???????{?return?data_[check(i)];?}
???Array(const?Array<T>&);
???Array<T>&?operator=?(const?Array<T>&);
?private:
???int?len_;
???T*??data_;
???int?check(int?i)?const
?????{?if?(i?<?0?||?i?>=?len_)?throw?BoundsViol("Array",?i,?len_);
???????return?i;?}
?};
```
與模板函數不同,模板類(實例化模板)在實例化時需要指明相關參數:
```
?int?main()
?{
???Array<int>???????????ai;
???Array<float>?????????af;
???Array<char*>?????????ac;
???Array<std::string>???as;
???Array<?Array<int>?>??aai;
...
?}
```
注意最后一個例子中的兩個`>`之間的空格符。如果沒有這個空格符,編譯器會看到一個`>>`(右移位)標記,而不是兩個`>`。
## 35.3 什么是“函數模板”的語法/語義?
考慮下面函數,交換兩個整型參數:
```
?void?swap(int&?x,?int&?y)
?{
???int?tmp?=?x;
???x?=?y;
???y?=?tmp;
?}
```
如果我們還要交換浮點數,長整形,字符串,集合,和文件系統等,我們就會疲于編寫除了類型不同的相似的編碼行。重復是電腦理想的工作,因此要用函數模板:
```
?template<typename?T>
?void?swap(T&?x,?T&?y)
?{
???T?tmp?=?x;
???x?=?y;
???y?=?tmp;
?}
```
對給定的類型每次我們使用swap()的時候,編譯器將根據上述定義,并自動產生另外一個“模板函數”作為上述函數模板的實例化。例如:
```
?int?main()
?{
???int?????????i,j;??/*...*/??swap(i,j);??//?Instantiates?a?swap?for?int
???float???????a,b;??/*...*/??swap(a,b);??//?Instantiates?a?swap?for?float
???char????????c,d;??/*...*/??swap(c,d);??//?Instantiates?a?swap?for?char
???std::string?s,t;??/*...*/??swap(s,t);??//?Instantiates?a?swap?for?std::string
...
?}
```
注:“模板函數”是一個“函數模板”的實例化形態。
## 35.4 如何確定顯式調用函數模板的哪個版本?
當你調用一個函數模板時,編譯器試圖推斷模板類型。大部分情況下,編譯器可以成功的做到這一點,但有時你可能想要幫助編譯器推斷出正確的類型-要么是因為它不能推斷出模板類型,或者是因為它會推斷出錯誤類型。
例如,你可能會調用一個函數模板沒有模板指定的參數類型,或者你可能想讓編譯器在選擇正確的函數模板之前,迫使它對參數做一些轉換(promotions)。在這些情況下,你需要明確地告訴編譯器應該調用函數的模板哪個實例化。
下面是一個示例函數模板,模板參數 T沒有出現在函數的參數列表中。在這種情況下, 編譯器無法推斷出模板參數類型在函數被調用時。
```
?template<typename?T>
?void?f()
?{
...
?}
```
若要調用該函數把 `T`作為`int`或`std::string`,你可以這樣做:
```
?#include?<string>
?void?sample()
?{
???f<int>();??????????//?type?T?will?be?int?in?this?call
???f<std::string>();??//?type?T?will?be?std::string?in?this?call
?}
```
這里是另一個函數,它的模板參數出現在函數的正式參數列表中(也就是說,編譯器_可以_根據實際參數的類型推導出模板類型):
```
?template<typename?T>
?void?g(T?x)
?{
...
?}
```
現在如果你想強制實行參數轉換,在編譯器推斷模板類型之前,你可以使用上述技術。例如,如果你只是簡單調用`g(42)`,你會得到`g<int>(42)`,但如果你想傳遞42給`g<long>()`,你可以這樣做: `g<long>(42)`。(當然你也可以明確地轉換參數,如可以`g(long(42))`,甚至`g(42L)`,當然如果這樣的話本例子就沒有什么意義了。)
同樣,如果你調用`g(“xyz”)`,你最終會調用`g<char*>(char*)`,但如果你想調用`std::string`版本`g<>()`,你可以這樣`g<std::string>(”xyz“)`。(同樣你也可以轉換參數,例如`g(std::string(“xyz”)`,不過那將是另一回事。)
## 35.5 什么是“參數化類型”?
換句話說,“類模板”。
參數化類型是一個類型,是參數化的類型或者值。 `list<int>`是一個被另外一個類型(`int`)參數化的類型( `List` )。
## 35.6 什么是“泛型”?
還是“類模板”另一種說法。
不要 與“一般性(generality)”混淆(“一般性(generality)”這只是避免過于具體的解決方案),“泛型”是指類模板。
## 35.7 當模板類型`T`是 `int`或`std::string`時,我的模板函數需要進行特殊處理。對特殊類型的T我該怎么實現模板特化?
在展示如何做到這一點之前,讓我們確保你不會搬起石頭砸自己的腳。對于用戶來說是否該函數的行為不同?換言之,是否可以觀察到的行為有實質性的不同?如果是這樣,你可能是在自找苦吃,你可能迷惑用戶--你最好使用不同名稱的函數--不要使用模板,不要使用重載。例如,如果接受`int`類型的代碼要插入一些東西到容器并且對結果排序,但接受`std::string`類型的代碼要從容器中刪除東西并且不對結果排序,這兩個函數不應該是可以重載的函數對--他們可以觀察的行為是不同的,所以他們應該有不同的函數名稱。
但是,如果該函數的可觀察到的行為是一致的,對于所有T類型僅僅局限在各自實現細節上的不同,那么就請繼續讀下去。讓我們看看這方面的一個例子(僅僅是概念上,不是C++代碼):
```
?template<typename?T>
?void?foo(const?T&?x)
?{
???switch?(typeof(T))?{??←?conceptual?only;?not?C++
?????case?int:
???????...??←?implementation?details?when?T?is?int
???????break;
?????case?std::string:
???????...??←?implementation?details?when?T?is?std::string
???????break;
?????default:
???????...??←?implementation?details?when?T?is?neither?int?nor?std::string
???????break;
???}
?}
```
解決上述問題的辦法就是是通過模板特化。不要使用`switch`語句,你需要把代碼分解成單獨的函數。第一個函數是默認的情況--當 `T`是`int`或`std::string`以外的任何其他類型時候的代碼:
```
?template<typename?T>
?void?foo(const?T&?x)
?{
???...??←?implementation?details?when?T?is?neither?int?nor?std::string
?}
```
下一步是兩個特例,第一個是`int`特例 的代碼:
```
?template<>
?void?foo<int>(const?int&?x)
?{
???...??←?implementation?details?when?T?is?int
?}
```
接著是`std::string`特例 的代碼:
```
?template<>
?void?foo<std::string>(const?std::string&?x)
?{
???...??←?implementation?details?when T?is?std::string
?}
```
好啦,大功告成!編譯器將自動選擇正確的特例實現根據所使用的`T`的類型。
## 35.8 哈?你能提供一個具體的模板特化的例子嗎??
可以。
下面我個人使用模板特化的幾種常見情況是字符串化。我通常使用模板, 將不同類型的對象字符串化,但通常需要字符串化某些特定的類型,例如當字符串化 布爾變量的時候,我喜歡用“true”與“false”來代替“1”和“0”,所以當 T 是布爾類型時,我使用std::boolalpha 。此外,我喜歡浮點輸出包含所有的數字(這樣我就可以看得很小的差異,等等),因此當 T是一個浮點類型時候,我使用`std::setprecision`。最終的結果通常如下所示:
```
?#include?<iostream>
?#include?<sstream>
?#include?<iomanip>
?#include?<string>
?#include?<limits>
?template<typename?T>?inline?std::string?stringify(const?T&?x)
?{
???std::ostringstream?out;
???out?<<?x;
???return?out.str();
?}
?template<>?inline?std::string?stringify<bool>(const?bool&?x)
?{
???std::ostringstream?out;
???out?<<?std::boolalpha?<<?x;
???return?out.str();
?}
?template<>?inline?std::string?stringify<double>(const?double&?x)
?{
???const?int?sigdigits?=?std::numeric_limits<double>::digits10;
//?or?perhaps?std::numeric_limits<double>::max_digits10?if?that?is?available?on?your?compiler
???std::ostringstream?out;
???out?<<?std::setprecision(sigdigits)?<<?x;
???return?out.str();
?}
?template<>?inline?std::string?stringify<float>(const?float&?x)
?{
???const?int?sigdigits?=?std::numeric_limits<float>::digits10;
//?or?perhaps?std::numeric_limits<float>::max_digits10?if?that?is?available?on?your?compiler
???std::ostringstream?out;
???out?<<?std::setprecision(sigdigits)?<<?x;
???return?out.str();
?}
?template<>?inline?std::string?stringify<long?double>(const?long?double&?x)
?{
???const?int?sigdigits?=?std::numeric_limits<long?double>::digits10;
//?or?perhaps?std::numeric_limits<long_double>::max_digits10?if?that?is?available?on?your?compiler
???std::ostringstream?out;
???out?<<?std::setprecision(sigdigits)?<<?x;
???return?out.str();
?}
```
從概念上來講他們都做同樣的事情:把參數字符串化。這意味著可觀察的行為是一致的,因此特化不會迷惑用戶。但對于`bool`和浮點類型,細節的實現略有不同,因此模板特化是一個好的解決方法。
## 35.9 但是模板函數的大部分代碼是相同的,是否有辦法實現模板特化并且不用重復復制所有的源代碼?
是。
例如,假設你的模板函數有很多共同的代碼,與類型T相關的特定代碼相對很少(僅僅是概念展示;不是C++):
```
?template<typename?T>
?void?foo(const?T&?x)
?{
...?common?code?that?works?for?all?T?types?...
???switch?(typeof(T))?{??←?conceptual?only;?not?C++
?????case?int:
...?small?amount?of?code?used?only?when?T?is?int?...
???????break;
?????case?std::string:
...?small?amount?of?code?used?only?when?T?is?std::string...
???????break;
?????default:
...?small?amount?of?code?used?when?T?is?neither?int?nor?std::string?...
???????break;
???}
...?more?common?code?that?works?for?all?T?types?...
?}
```
如果盲目地跟從模板特化FAQ的建議,你最終將需要重復`switch`語句之前和之后的所有代碼。兩全其美的方式—既不重復相同代碼又可以實現`T`的特定代碼,是分離`switch`語句到一個單獨的函數`foo_part()`,并使用模板特殊化:
```
?template<typename?T>?inline?void?foo_part(const?T&?x)
?{
...?small?amount?of?code?used?when?T?is?neither?int?nor?std::string?...
?}
?template<>?inline?void?foo_part<int>(const?int&?x)
?{
...?small?amount?of?code?used?only?when?T?is?int?...
?}
?template<>?inline?void?foo_part<std::string>(const?std::string&?x)
?{
...?small?amount?of?code?used?only?when?T?is?std::string?...
?}
```
主要的`foo()`函數是一個簡單的模板-沒有特化。請注意,`switch`語句已經被替換為`foo_part()`調用:
```
?template<typename?T>
?void?foo(const?T&?x)
?{
...?common?code?that?works?for?all?T?types?...
???foo_part(x);
...?more?common?code?that?works?for?all?T?types?...
?}
```
正如你所看到的, `foo()`的函數體本身并沒有任何特殊,這一切都會自動的被調用。編譯器自動生成的基于 `T`類型 的`foo()`,并會生成正確的`foo_part`函數,根據實際編譯時的`X`的參數類型。合適的`foo_part`的特化會被實例化。
## 35.10 所有這些模板和模板特化都會降低程序執行速度,對不對?
錯誤的。
這與實現代碼的質量有關,結果可能會有所不同。但是不會有任何降低。模板可能會些微影響編譯速度,但一旦類型在編譯時被確定,它通常會生成和非模板函數(包括內聯展開等)一樣快的代碼。
## 35.11 因此模板重載了函數,對不對?
是也不是。
函數模板參與重載函數的名稱解析,但規則是不同的。對于模板重載,類型需要完全匹配。如果類型不完全匹配,類型不會被轉換,函數模板從可行的函數集合中被排除。這就是所謂的“SFINAE”- Subsitution Failure Is Not An Error。例如:
```
?#include?<iostream>
?#include?<typeinfo>
?template<typename?T>?void?foo(T*?x)
?{?std::cout?<<?"foo<"?<<?typeid(T).name()?<<?">(T*)\n";?}
?void?foo(int?x)
?{?std::cout?<<?"foo(int)\n";?}
?void?foo(double?x)
?{?std::cout?<<?"foo(double)\n";?}
?int?main()
?{
?????foo(42);????????//?matches?foo(int)?exactly
?????foo(42.0);??????//?matches?foo(double)?exactly
?????foo("abcdef");??//?matches?foo<T>(T*)?with?T?=?char
?????return?0;
?}
```
在這個例子中, 在main()函數中第一或第二次調用`foo`不是對`foo<T>`的調用,因為無論42還是42.0都沒有提供給編譯器的任何信息來推斷 。然而第三個調用,包括`foo<T>`并且`T = char`,因此它會調用`foo<T>`。
## 35.12 為什么不能分開模板的聲明和定義,把定義放到`.cpp`文件中?
如果你想知道的是只是如何解決這種情況,請閱讀下面得兩個s。但是,為了理解要那樣,首先接受這些事實:
1. 模板是不是一個類或函數。 模板是一個“模式”,編譯器用來生成的相似的類或者函數。
2. 為了讓編譯器生成的代碼,它必須同時看到模板的定義(不只是聲明)和特定類型/任何用于“fill in”模板的類型。例如,如果你想使用一個`foo<int>`,編譯器必須同時看到foo模板和你要調用具體的`foo<int>`。
3. 編譯器可能不記得另外一個`.cpp`文件的細節,當編譯其他`.cpp`文件的時候。它可以 ,但大多數都沒有,如果你正在閱讀本FAQ,它幾乎肯定不會。順便說一句,這就是所謂的“獨立編譯模型”。
現在,基于這些事實,下面是一個范例,它表明為什么是這個樣子。假設你有一個這樣的模板`Foo`聲明:
```
?template<typename?T>
?class?Foo?{
?public:
???Foo();
???void?someMethod(T?x);
?private:
???T?x;
?};
```
類似地,模板成員函數的定義:
```
?template<typename?T>
?Foo<T>::Foo()
?{
...
?}
?template<typename?T>
?void?Foo<T>::someMethod(T?x)
?{
...
?}
```
現在,假設在文件`Bar.cpp`的一些代碼要使用`foo<int>`:
```
//?Bar.cpp
?void?blah_blah_blah()
?{
...
???Foo<int>?f;
???f.someMethod(5);
...
?}
```
顯然,某人某地將不得不調用“模式”的構造函數,和`someMethod()`函數以及做`T`為`int`的實例化。但是,如果你把構造函數和`someMethod()`的定義放到文件`Foo.cpp`,當編譯`Foo.cpp`時,編譯器將看到模板代碼;當編譯`Bar.cpp`時,編譯器將看到`foo<int>`。但任何時候決不會同時看到模板代碼和`foo<int>`。因此,通過上面的2號規則,它根本不會產生`foo <int>::someMethod()`的代碼。
_寫給專家們的話:很明顯我對以上內容作了簡化。這是有意為之,所以請不要大聲抱怨。_如果你知道`.cpp`文件和編譯單元的差別,類模板和模板類的差別,模板其實不只是美化的宏等,請不要抱怨:這個問題/解答不是為你而設。我簡化它是為了新手能夠“理解它”,即使這樣可能會冒犯一些專家。
_提醒:_欲知解決方案,請閱讀下面得兩個 FAQs。
## 35.13 如何避免模板函數的鏈接錯誤?
當編譯模板函數的`.cpp`文件的時候告訴C++編譯器應該使用哪個實例。
例如,考慮`foo.h`頭文件包含以下模板函數聲明:
```
//?File?"foo.h"
?template<typename?T>
?extern?void?foo();
```
現在假設文件`foo.cpp`實際上定義的模板函數:
```
//?File?"foo.cpp"
?#include?<iostream>
?#include?"foo.h"
?template<typename?T>
?void?foo()
?{
???std::cout?<<?"Here?I?am!\n";
?}
```
假設文件`main.cpp`中使用這個模板函數通過調用`foo<int>()`:
```
//?File?"main.cpp"
?#include?"foo.h"
?int?main()
?{
???foo<int>();
...
?}
```
如果你編譯和(試圖)鏈接這兩個`.cpp`文件,大多數編譯器將生成鏈接錯誤。有三種的解決方案。第一個解決方案是物理上在`.h`文件中定義,即使它不是一個內聯函數。這種解決辦法可能(或可能不會!)造成重大代碼膨脹,意味著可執行文件的大小可能會顯顯著增加(或者,如果你的編譯器足夠聰明,可能不會這么做)。
另一個解決辦法是保留定義在`.cpp`文件中,只添加行`template void foo<int>()`到`.cpp`文件:
```
//?File?"foo.cpp"
?#include?<iostream>
?#include?"foo.h"
?template<typename?T>?void?foo()
?{
???std::cout?<<?"Here?I?am!\n";
?}
?template?void?foo<int>();
```
如果你不能修改`foo.cpp`,只需創建一個新的`.cpp`文件,例如`foo-impl.cpp`如下:
```
//?File?"foo-impl.cpp"
?#include?"foo.cpp"
?template?void?foo<int>();
```
請注意, `foo-impl.cpp`文件包含`.cpp`文件,而不是`.h`文件。如果你覺著這樣很亂,跳個踢踏舞,想想堪薩斯,跟著我重復,“我要這么做即使它很混亂。” 你需要信任我。如果不信任或者致使好奇,前面的FAQ給出了理由。
## 35.14 如何使用C++的關鍵字`export`來避免模板鏈接錯誤??
C++關鍵字`export`是設計用來消除包含一個模板定義(無論是在頭文件中或通過實現文件中)的需要。但是,在寫這篇文章時,支持此功能的唯一的知名編譯器,是[Comeau C++](http://www.comeaucomputing.com/tryitout)。`export`關鍵字未來還是個未知數。說句公道話,一些編譯器廠商表示他們可能永遠不會實現它,而C++標準委員會已決定大家自己定奪。
在不支持關鍵字`export`的編譯器上,如果你希望你的代碼可以通過編譯,并且還希望能夠有效利用支持`export`關鍵字的編譯器。你可以這樣定義模板頭文件:
```
//?File?Foo.h
?template<typename?T>
?class?Foo?{
...
?};
?#ifndef?USE_EXPORT_KEYWORD
???#include?"Foo.cpp"
?#endif
```
并定義非內聯函數的源代碼文件如下:
```
//?File?Foo.cpp
?#ifndef?USE_EXPORT_KEYWORD
???#define?export?/*nothing*/
?#endif
?export?template<typename?T>?...
```
然后,如果/當你的編譯器支持`export`關鍵字的時候,并且因為某些原因你想利用該功能,只要定義符號`USE_EXPORT_KEYWORD`即可。
要訣就是,你現在可以開發程序, 好像你的編譯器已經實現了`export`關鍵字。如果/當你的編譯器真正支持該關鍵字的時候,只需要定義`USE_EXPORT_KEYWORD`標志,重新編譯,馬上你就可以利用該功能。
## 35.15 如何避免模板類的鏈接錯誤?
當編譯模板類的`.cpp`文件得手告訴你的C++編譯器應該使用哪個模板實例。(如果你已經閱讀以前的問題,答案是完全一樣的,所以你也許可以跳過此答案。)
作為一個例子,考慮`Foo.h`頭文件包含以下模板類。請注意, `Foo<T>::f()`方法是內聯的,而`Foo<T>::g()`和`Foo<T>::h()`卻不是。
```
//?File?"Foo.h"
?template<typename?T>
?class?Foo?{
?public:
???void?f();
???void?g();
???void?h();
?};
?template<typename?T>
?inline
?void?Foo<T>::f()
?{
...
?}
```
現在,假設文件`Foo.cpp`實際定義了非內聯的`Foo<T>::g()`和`Foo<T>::h()`:
```
//?File?"Foo.cpp"
?#include?<iostream>
?#include?"Foo.h"
?template<typename?T>
?void?Foo<T>::g()
?{
???std::cout?<<?"Foo<T>::g()\n";
?}
?template<typename?T>
?void?Foo<T>::h()
?{
???std::cout?<<?"Foo<T>::h()\n";
?}
```
假設文件`main.cpp`使用該模板創建一個`Foo<int>`并調用其方法:
```
//?File?"main.cpp"
?#include?"Foo.h"
?int?main()
?{
???Foo<int>?x;
???x.f();
???x.g();
???x.h();
...
?}
```
如果你編譯和(試圖)鏈接這兩個`.cpp`文件,大多數編譯器將生成鏈接錯誤。有三種的解決方案。第一個解決方案是物理上在`.h`文件中定義,即使它不是一個內聯函數。這種解決辦法可能(或可能不會!)造成重大代碼膨脹,意味著可執行文件的大小可能會顯顯著增加(或者,如果你的編譯器足夠聰明,可能不會這么做)。
另一個解決辦法是保留定義在`.cpp`文件中,只添加行`template class Foo<int>;`到`.cpp`文件:
```
//?File?"Foo.cpp"
?#include?<iostream>
?#include?"Foo.h"
...definition?of?Foo<T>::f()?is?unchanged?--?see?above...
...definition?of?Foo<T>::g()?is?unchanged?--?see?above...
?template?class?Foo<int>;
```
如果你不能修改`foo.cpp`,只需創建一個新的`.cpp`文件,例如`foo-impl.cpp`如下:
```
//?File?"Foo-impl.cpp"
?#include?"Foo.cpp"
?template?class?Foo<int>;
```
請注意, `foo-impl.cpp`文件包含`.cpp`文件,而不是`.h`文件。如果你覺著這樣很亂,跳個踢踏舞,想想堪薩斯,跟著我重復,“我要這么做即使它很混亂。” 你需要信任我。如果不信任或者致使好奇,前面的FAQ給出了理由。
如果你使用[Comeau C++](http://www.comeaucomputing.com/tryitout "www.comeaucomputing.com/tryitout"),你可能使用`export`關鍵字實現類似功能。
## 35.16 為什么我收到鏈接錯誤 ,當我使用模板友元的時候?
由于模板友類的復雜性。下面是一個常見的例子:
```
?#include?<iostream>
?template<typename?T>
?class?Foo?{
?public:
???Foo(const?T&?value?=?T());
???friend?Foo<T>?operator+?(const?Foo<T>&?lhs,?const?Foo<T>&?rhs);
???friend?std::ostream&?operator<<?(std::ostream&?o,?const?Foo<T>&?x);
?private:
???T?value_;
?};
```
當然在某個地方我們會用到模板:
```
?int?main()
?{
???Foo<int>?lhs(1);
???Foo<int>?rhs(2);
???Foo<int>?result?=?lhs?+?rhs;
???std::cout?<<?result;
...
?}
```
當然,在某個地方需要定義各成員和友元函數:
```
?template<typename?T>
?Foo<T>::Foo(const?T&?value?=?T())
???:?value_(value)
?{?}
?template<typename?T>
?Foo<T>?operator+?(const?Foo<T>&?lhs,?const?Foo<T>&?rhs)
?{?return?Foo<T>(lhs.value_?+?rhs.value_);?}
?template<typename?T>
?std::ostream&?operator<<?(std::ostream&?o,?const?Foo<T>&?x)
?{?return?o?<<?x.value_;?}
```
一個潛在問題是編譯器如何理解類聲明中的friends行。在看到friends行的時候,它還不知道友元函數本身也是模板,它假定他們不是模板函數,就像下面這樣:
```
?Foo<int>?operator+?(const?Foo<int>&?lhs,?const?Foo<int>&?rhs)
?{?...?}
?std::ostream&?operator<<?(std::ostream&?o,?const?Foo<int>&?x)
?{?...?}
```
當你調用運算符`+`或運算符`<<`的時候,這種假設導致編譯器生成一個對非模板函數的調用,但是鏈接器會給你一個“未定義的外部函數”錯誤,因為你從來沒有真正的定義這些非模板函數。
解決的辦法是在編譯器編譯類體的時候,讓編譯器知道運算符`+`和運算符`<<`本身是模板。有幾種方法可以做到這一點;一個簡單的方法是在定義函數模板類 Foo的時候預先聲明模板友元:
```
?template<typename?T>?class?Foo;??//?pre-declare?the?template?class?itself
?template<typename?T>?Foo<T>?operator+?(const?Foo<T>&?lhs,?const?Foo<T>&?rhs);
?template<typename?T>?std::ostream&?operator<<?(std::ostream&?o,?const?Foo<T>&?x);
```
在`frend`行中你也需要加入`<>`,如下所示:
```
?#include?<iostream>
?template<typename?T>
?class?Foo?{
?public:
???Foo(const?T&?value?=?T());
???friend?Foo<T>?operator+?<>?(const?Foo<T>&?lhs,?const?Foo<T>&?rhs);
???friend?std::ostream&?operator<<?<>?(std::ostream&?o,?const?Foo<T>&?x);
?private:
???T?value_;
?};
```
這些寫法將有助于編譯器更好地了解友元函數。值得一提的是,它會發現友元函數本身是模板。這消除了混亂。
另一種方法是在類中同時聲明和定義該友元函數。例如:
```
?#include?<iostream>
?template<typename?T>
?class?Foo?{
?public:
???Foo(const?T&?value?=?T());
???friend?Foo<T>?operator+?(const?Foo<T>&?lhs,?const?Foo<T>&?rhs)
???{
...
???}
???friend?std::ostream&?operator<<?(std::ostream&?o,?const?Foo<T>&?x)
???{
...
???}
?private:
???T?value_;
?};
```
## 35.17 怎么理解這些繁雜的模板錯誤信息?
這里有一個免費工具, [可以轉換錯誤信息便于理解](http://www.bdsoft.com/tools/stlfilt.html)。在撰寫本文的時候,它工作用于下列編譯器:Comeau C +,Intel C++,CodeWarrior C++,gcc,Borland C++,Microsoft Visual C++和EDG C++。
這里有一個例子,下面是一些原始的gcc的錯誤信息:
```
?rtmap.cpp:?In?function?int?main()':
?rtmap.cpp:19:?invalid?conversion?from?int'?to?
????std::_Rb_tree_node<std::pair<const?int,?double>?>*'
?rtmap.cpp:19:???initializing?argument?1?of?std::_Rb_tree_iterator<_Val,?_Ref,
????_Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*)?[with?_Val?=
????std::pair<const?int,?double>,?_Ref?=?std::pair<const?int,?double>&,?_Ptr?=
????std::pair<const?int,?double>*]'
?rtmap.cpp:20:?invalid?conversion?from?int'?to?
????std::_Rb_tree_node<std::pair<const?int,?double>?>*'
?rtmap.cpp:20:???initializing?argument?1?of?std::_Rb_tree_iterator<_Val,?_Ref,
????_Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*)?[with?_Val?=
????std::pair<const?int,?double>,?_Ref?=?std::pair<const?int,?double>&,?_Ptr?=
????std::pair<const?int,?double>*]'
?E:/GCC3/include/c++/3.2/bits/stl_tree.h:?In?member?function?void
????std::_Rb_tree<_Key,?_Val,?_KeyOfValue,?_Compare,?_Alloc>::insert_unique(_II,
?????_II)?[with?_InputIterator?=?int,?_Key?=?int,?_Val?=?std::pair<const?int,
????double>,?_KeyOfValue?=?std::_Select1st<std::pair<const?int,?double>?>,
????_Compare?=?std::less<int>,?_Alloc?=?std::allocator<std::pair<const?int,
????double>?>]':
?E:/GCC3/include/c++/3.2/bits/stl_map.h:272:???instantiated?from?void?std::map<_
?Key,?_Tp,?_Compare,?_Alloc>::insert(_InputIterator,?_InputIterator)?[with?_Input
?Iterator?=?int,?_Key?=?int,?_Tp?=?double,?_Compare?=?std::less<int>,?_Alloc?=?st
?d::allocator<std::pair<const?int,?double>?>]'
?rtmap.cpp:21:???instantiated?from?here
?E:/GCC3/include/c++/3.2/bits/stl_tree.h:1161:?invalid?type?argument?of?unary?*
????'
```
以下是經過過濾的錯誤信息(注:你可以配置工具讓它顯示更多的信息,下面輸出的設置是剪裁信息到最少):
```
?rtmap.cpp:?In?function?int?main()':
?rtmap.cpp:19:?invalid?conversion?from?int'?to?iter'
?rtmap.cpp:19:???initializing?argument?1?of?iter(iter)'
?rtmap.cpp:20:?invalid?conversion?from?int'?to?iter'
?rtmap.cpp:20:???initializing?argument?1?of?iter(iter)'
?stl_tree.h:?In?member?function?void?map<int,double>::insert_unique(_II,?_II)':
?????[STL?Decryptor:?Suppressed?1?more?STL?standard?header?message]
?rtmap.cpp:21:???instantiated?from?here
?stl_tree.h:1161:?invalid?type?argument?of?unary?*'
```
以下是上面例子的源代碼:
```
?#include?<map>
?#include?<algorithm>
?#include?<cmath>
?const?int?values[]?=?{?1,2,3,4,5?};
?const?int?NVALS?=?sizeof?values?/?sizeof?(int);
?int?main()
?{
?????using?namespace?std;
?????typedef?map<int,?double>?valmap;
?????valmap?m;
?????for?(int?i?=?0;?i?<?NVALS;?i++)
?????????m.insert(make_pair(values[i],?pow(values[i],?.5)));
?????valmap::iterator?it?=?100;??????????????//?error
?????valmap::iterator?it2(100);??????????????//?error
?????m.insert(1,2);??????????????????????????//?error
?????return?0;
?}
```
## 35.18 當模板派生類使用一個繼承自模板基類的嵌套類型時,為什么出錯??
你也許很吃驚,下面的代碼是無效的C++代碼,即使如此通過有些編譯器:
```
?template<typename?T>
?class?B?{
?public:
???class?Xyz?{?...?};??←?type?nested?in?class?B<T>
???typedef?int?Pqr;????←?type?nested?in?class?B<T>
?};
?template<typename?T>
?class?D?:?public?B<T>?{
?public:
???void?g()
???{
?????Xyz?x;??←?bad?(even?though?some?compilers?erroneously?(temporarily?)?accept?it)
?????Pqr?y;??←?bad?(even?though?some?compilers?erroneously?(temporarily?)?accept?it)
???}
?};
```
這可能會讓你很傷腦筋,最好坐下來聽我講。
在函數`D<T>::g()`內,名字`xyz`和`Pqr`不依賴于模板參數`T`,所以他們被稱作為nondependent名字。另一方面`B<T>` 依賴模板參數`T`,因此 `B<T>` 稱作_dependent名字_。
規則是這樣的:當查找nondependent名字(比如`Xyz`和`Pqr`)的時候,編譯器不會查找dependent基類(如`B <T>`中 )。因此,編譯器不知道他們甚至還存在,更不用說知道它們也是類型。
這時,程序員有時會添加前綴`B <T>::`,例如:
```
?template<typename?T>
?class?D?:?public?B<T>?{
?public:
???void?g()
???{
?????B<T>::Xyz?x;??←?bad?(even?though?some?compilers?erroneously?(temporarily?)?accept?it)
?????B<T>::Pqr?y;??←?bad?(even?though?some?compilers?erroneously?(temporarily?)?accept?it)
???}
?};
```
可惜這也行不通,因為這些名字(你準備好了嗎?坐下來?)不一定是類型。 "哈?!?" ?"不是類型?!?" ?。“太搞了吧!任何傻瓜都可以看到他們是類型;只要看上一眼!”,你抗議。抱歉,事實是,他們可能不是類型。原因是,有可能是`B<T>`的特化,假設`B<Foo>`,其中 `B <Foo>::Xyz`是一個數據成員。由于這種潛在的特化,編譯器不能假設`B<T>::Xyz`是一個類型,直到它知道`T `。解決方案是通過`typename`關鍵字提示編譯器:
```
?template<typename?T>
?class?D?:?public?B<T>?{
?public:
???void?g()
???{
?????typename?B<T>::Xyz?x;??←?good
?????typename?B<T>::Pqr?y;??←?good
???}
?};
```
## 35.19 當模板派生類使用使用一個繼承自模板基類的成員變量時 ,為什么出錯? ?
你也許很吃驚,下面的代碼是無效的C++代碼,即使如此通過有些編譯器:
```
?template<typename?T>
?class?B?{
?public:
???void?f()?{?}??←?member?of?class?B<T>
?};
?template<typename?T>
?class?D?:?public?B<T>?{
?public:
???void?g()
???{
?????f();??←?bad?(even?though?some?compilers?erroneously?(temporarily?)?accept?it)
???}
?};
```
這可能會讓你很傷腦筋,最好坐下來聽我講。
在函數`D<T>::g()`內,名字`f`不依賴于模板參數`T`,所以他們被稱作為nondependent名字。另一方面`B<T>` 依賴模板參數`T`,因此 `B<T>` 稱作_dependent名字_。
規則是這樣的:當查找nondependent名字(比如f)的時候,編譯器不會查找dependent基類(如`B <T>`中 )。
這并不意味著繼承不起作用。類`D <int>`是仍然繼承自類`B <int>`,編譯器仍然讓你可以隱式的做is- a轉換(例如,`D<int>*`到 `B <int> *`),動態綁定仍然有效當虛函數被調用時,等等。但有一個如何查找名稱的問題。
替代方案:
* 改變的`f()`的調用為`this->f()`。由于在模板中`this`指針一直是隱式實現的,`this->f()`要依賴查找,因此推遲到模板實例化時,此時所有基類都會被查找。
* 在調用`f()`之前,插入 `using B<T>::f;`語句。
* 改變的`f()`的調用為`B <T>::f()`。 但是請注意,如果`f()`是虛函數,這可能沒有給你想要的東西,因為它禁止了虛函數帶調用機制。
## 35.20 前一個問題可以暗傷我?難道編譯器默認地產生錯誤代碼??
是。
由于non-dependent類型 and non-dependent成員不會在dependent模板在基礎類中搜索,編譯器將搜索封閉范圍,比如封閉名字空間。這可能會導致它在你沒有意識到的情況下(!)做錯誤的事情。
例如:
```
?class?Xyz?{?...?};??←?global?("namespace?scope")?type
?void?f()?{?}????????←?global?("namespace?scope")?function
?template<typename?T>
?class?B?{
?public:
???class?Xyz?{?...?};??←?type?nested?in?class?B<T>
???void?f()?{?}????????←?member?of?class?B<T>
?};
?template<typename?T>
?class?D?:?public?B<T>?{
?public:
???void?g()
???{
?????Xyz?x;??←?suprise:?you?get?the?global?Xyz!!
?????f();????←?suprise:?you?get?the?global?f!!
???}
?};
```
`D<T>::g()`內的`Xyz`和`f`將被解析為全局變量,而不是繼承自類`B <T>`,這恐怕不是你的真正意圖。
別埋怨我沒有警告過你。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫