我正在用一個基於模板的庫源代碼,該庫包含一些針對特定類型的模板函數特化。類模板,函數模板和模板函數特化都在頭文件中。我在我的.cpp文件中 #include 頭文件並編譯鏈接工程。但是為了在整個工程中使用該庫,我將頭文件包含在 stdafx.h 中,結果出現特化模板函數的符號多重定義錯誤。我要如何組織頭文件才能避免多重符號定義錯誤?我用 /FORCE:MULTIPLE,但我想用一個更好的解決方法。
實際上,確實用更好的解決方法。稍后我會解釋,但首先讓我重溫一下模板函數特化是如何工作的。假設你有一個比較兩個基於 operator> 和 operator== 對象的模板函數:
template <typename T>
int compare(T t1, T t2)
{
return t1==t2 ? 0 : t1 > t2 ? 1 : -1;
}
該模板根據地一個參數是否等於、大於、或小於第二個參數而分別返回零或+/-1。它是典型的用於集合排序時的排序函數。它假設類型 T 具備 operator== 和 operator> 操作,並支持 int,float,double 或 DWORD 類型。但它不能應用於比較自負串(char* 指針),因為這個函數比較的是串指針,而不是字符串本身:
LPCTSTR s1,s2;
...
int cmp = compare(s1,s2); // s1<s2? Oops!
為了能進行字符串比較,你需要一個使用 strcmp 或其 TCHAR 版本 _tcscmp 的模板特化:
// specialization for strings
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
沒錯,這樣做完全正確,現在的問題是:將這個特化放在何處?顯然是要放在模板的頭文件中。但這樣會導致符號多重定義的錯誤,就像 Lee 遇到的那樣。原因很明顯,模板特化是一個函數,而非模板。它與下面的寫法是一樣的:
int compare(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
沒有理由不在頭文件中定義函數——但是一旦這樣做了,那么你便無法在多個文件中 #include 該頭文件。至少,肯定會有鏈接錯誤。怎么辦呢?
如果你掌握了模板函數特化即函數,而非模板的概念,你就會認識到有三個選項,完全與普通函數一樣;特化為 inline,extern 或者 static。例如,像下面這樣:
template<>
inline int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
對於大多數模板庫而言,這是最容易和最常見的解決方案。因為編譯器直接擴展內聯函數,不產生外部符號,在多個模塊中 #include 它們沒有什么問題。鏈接器不會出錯,因為不存在多重定義的符號。對於像 compare 這樣的小函數來說,inline 怎么說都是你想要的(它更快)。
但是,如果你的特化很長,或出於某種原因,你不想讓它成為 inline,那要如何做呢?此時可以做成 extern。語法與常規函數一樣:
// in .h header file
template<>
extern int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2);
當然,你得在某個地方實現 compare。部分細節如 Figure 7 所示。我在單獨的模塊 Templ.cpp 中實現了特化,它與主工程鏈接。Templ.h 被 #include 在 stdafx.h 中,而 stdafx.h 又被 #include 在 Templ.cpp 和主模塊兩個文件中——生成工程沒有鏈接錯誤。去下載源代碼自己嘗試一下吧。
如果你正在為其他開發人員寫模板庫,extern 方式會很不爽,因為你必須創建一個帶目標模塊的鏈接庫(lib),它包含有特化。如果你已經有了一個這樣的 .lib,也沒什么;如果沒有,你可能會想方設法避免引入這樣的庫。僅用頭文件實現模板是更好的方法(麻煩少)。最容易的方式是用 inline,此外,你還能將你的特化放在單獨的頭文件中,使之與其聲明分開並要其他開發人員只在一個模塊中 #include 特化。還有一個可選的方法是將所有東西放在一個文件中,並用預處理符號控制實例化:
#ifdef MYLIB_IMPLEMENT_FUNCS
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
#endif
使用該方法,所有模塊都包含此頭文件,但在包含它之前,只有一個 #define MYLIB_IMPLEMENT_FUNCS。這個方法不支持預編譯頭,因為編譯器用 stdafx.h 中的任何 MYLIB_IMPLEMENT_FUNCS 值加載預編譯版本。
避免符號多重定義錯誤的最后同時也是用得最少的一個方法是將特化做成 static:
template<>
static int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
這樣鏈接器也不會出錯,因為靜態函數不向外界輸出其函數,並且它讓你將所有東西都保持在一個頭文件中,不用引入預處理符號。但它缺乏效率,因為每個模塊都有一個函數拷貝。如果函數小到沒什么——那為何不用內聯呢?
所以簡言之:將特化做成 inline 或 extern。通常都是用 inline。兩種方法都得編輯頭文件。如果使用的是第三方的庫沒有頭文件,那么你除了用鏈接選項 /FORCE:MULTIPLE 之外別無選擇。在你等着生成你的工程時,你可以告訴編寫庫文件的那個家伙——為什么要將函數模板特化定義成 inline 或者 extern。就說是我說的。
------------
c++模板概念
typename名字能更清楚的表明后面的名字是類型名,但是關鍵字typename是最近加入到標准C++中
(16)編譯器如何分析模板定義:(編譯時刻分析模板定義(注:不是模板實例化))
對於編譯器來說,它並不總是能夠區分出模板定義中的哪些表達式是類型. 為了讓編譯器能夠分析模板定義用戶必須指示編譯器哪些表達式是類型表達式, 告訴編譯器一個表達式是類型表達式的機制是在表達式前加上關鍵字typename.
(17)模板類型參數:
由關鍵字class 或typename 后加一個標識符構成.在函數的模板參數表中. 這兩個關鍵字的意義相同.它們表示后面的參數名代表一個潛在的內置或用戶定義的類型,模板參數名由程序員選擇. 模板類型參數被用作一個類型指示符可以出現在模板定義的余下部分. 1.模板類型參數名可以被用來指定函數模板的返回位.(函數的返回類型) 2.模板參數名在同一模板參數表中只能被使用一次,但是模板參數名可以在多個函數模板聲明或定義之間被重復使用. 3.模板參數在函數參數表中可以出現的次數沒有限制 4.一個模板的定義和多個聲明所使用的模板參數名無需相同 5.如果一個函數模板有一個以上的模板類型參數,則每個模板類型參數前面都必須有關鍵字class 或typename. 6.多個函數實參可以參加同一個模板實參的推演過程。如果模板參數在函數參數表中出現多次,則每個推演出來的類型都必須與根據模板實參推演出來的第一個類型完全匹配。 這些可能的類型轉換的限制只適用於參加模板實參推演過程的函數實參,對於所有其他實參所有的類型轉換都是允許的. 7. (18)模板非類型參數: 由一個普通的參數聲明構成,模板非類型參數表示該參數名代表了一個 潛在的值,而該值代表了模板定義中的一個常量. 模扳非類型參數被 用作一個常量值可以出現在模板定義的余下部分它可以用在要求常量的地方或許是在 數組聲明中指定數組的大小或作為枚舉常量的初始值. (19)模板的定義: 關鍵字template 總是放在模板的定義與聲明的最前面關鍵字后面是用逗號分隔的模板 參數表template parameter list 它用尖括號<> 一個小於號和一個大於號括起來. 該列表是模板參數表不能為空,模板參數可以是一個模板類型參數template type parameter 它代表了一種類型,也可以是一個模板非類型參數template nontype parameter 它代表了一個常量表達式.
函數定義或聲明跟在模板參數表
(20)模板實例化: 類型和值的替換過程被稱為模板實例化template instantiation. 函數模板指定了怎樣根據一組或更多實際類型或值構造出獨立的函數.這個構造過程被 稱為模板實例化template instantiation 這個過程是隱式發生的,它可以被看作是函數模板調用或取函數模板的地址的副作用。
(21)模板參數表:
用逗號分隔的模板參數表template parameter list 它用尖括號<> 一個小於號和一個大於號括起來. 該列表是模板參數表不能為空,模板參數可以是一個模板類型參數template type parameter 它代表了一種類型,也可以是一個模板非類型參數template nontype parameter 它代表了一個常量表達式.
(22)函數參數表:
(23)模板實參推演:(函數的返回值類型能推演否?)
用函數實參的類型來決定模板實參的類型和值的過程被稱為模板實參推演template argument deduction. 我們也可以不依賴模板實參推演過程而是顯式地指定模板實參。 在取函數模板實例的地址時必須能夠通過上下文環境為一個模板實參決定一個惟一的類型或值, 如果不能決定出這個惟一的類型或值就會產生編譯時刻錯誤.
當函數模板被調用時,對函數實參類型的檢查決定了模板實參的類型和值.這個過程被
稱為模板實參推演template argument deduction ****在模板實參推演期間決定模板實參的類型時編譯器不考慮函數模板實例的返回類型。 要想成功地進行模板實參推演,函數實參的類型不一定要嚴格匹配相應函數參數的類型. 下列三種類型轉換是允許的: 1.左值轉換: 2.限定轉換: 3.到一個基類該基類根據一個類模板實例化而來的轉換讓:
(24)顯式地指定模板實參
在某些情況下編譯器不可能推演出模板實參的類. 在這種情況下我們需要改變模板實參推演機制,並使用顯式指定explicitly specify 模板實參.模板實參被顯式指定在逗號分隔的列表中用尖括號<> 一個小於號和一個 大於號括起來緊跟在函數模板實例的名字后面.
但是當模板實參被顯式指定時就沒有必要推演模板實參了.
我們必須指出顯式模板實參應該只被用在完全需要它們來解決二義性或在模板實參 不能被推演出來的上下文中使用模板實例時首先讓編譯器來決定模板實參的類型和值是 比較容易的其次如果我們通過修改程序中的聲明來改變在函數模板實例調用中的函數實參的類型則編譯器會自動用不同的模板實參實例化函數模板而無需我們做任何事情另
一方面如果我們指定了顯式模板參數則必須檢查顯式模板實參對於函數實參的新類型是
否仍然合適所以建議在可能的時候省略顯式模板實參 (25)模板中返回值的問題 (26)顯式模板實參 c++ primer 3e (重要) (27)C++模板編譯模式template compilation model (28)函數的"template參數推導機制"推而導之的只是參數,無法推導函數的返回值類型。 (29)函數模板顯式特化c++ primer 3e 在模板顯式特化定義explicit specialization definition 中先是關鍵字template 和一對 尖括號(<> 一個小於號和一個大於號),然后是函數模板特化的定義,該定義指出了模板 名,被用來特化模板的模板實參以及函數參數表和函數體.
1.我們也可以聲明一個函數模板的顯式特化而不定義
2.在聲明或定義函數模板顯式特化時我們不能省略顯式特化聲明中的關鍵字template 及其后的尖括號類似地函數參數表也不能從特化聲明中省略掉. 3.但是如果模板實參可以從函數參數中推演出來則模板實參的顯式特化可以從顯式特化聲明中省略 4. (30)類模板顯式特化 與 Tratis c++ primer 3e (31)為類模板實例的一個成員提供一個特化定義 顯式特化定義包括關鍵字template 后跟一對尖括號(<>一個小於號和一個大於號)以及后面的類成員的特化定義.
(31)特化整個類模板
1.只有當通用的類模板被聲明(不一定被定義)之后它的顯式特化才可以被定義. 即,在模板被特化之前編譯器必須知道類模板的名字. 2.如果整個類被特化了,那么標記特化定義的符號template<>只能被放在類模板的顯式 特化的定義之前,類模板特化的成員定義不能以符號template<>作為打頭. (32)類模板部分特化 還是一個模板,只是部分模板參數通過具體的類型特化了. 如果類模板有一個以上的模板參數,則有些人就可能希望為一個特定的模板實參或者一 組模板實參特化類模吧,而不是為所有的模板參數特化該類模板.即,有人可能希望提供這樣一個模吧, 它仍然是一個通用的模吧,只不過某些模板參數已經被實際的類型或值取代. 通過使用類模板部分特化partial specialization ,這是有可能實現的.相比"通用模板定義 針對一組特定的模板實參被實例化之后的類版本"而言,類模板的部分特化可能被用來定義 一個更加適當更加高效的實現版本. 1.但是類模板部分特化的名字后面總是跟着一個模板實參表. 2.而部分特化的模板參數表只列出模板實參仍然未知的那些參數. 3.部分特化的定義與通用模板的定義完全無 |