[C++] 模板進(jìn)階:特化與編譯鏈接全解析

非類型模板
模板參數(shù)分為:類型形參和非類型形參
類型形參
類型形參,即在模板初階中所用的例如class A或typename A此類參數(shù)類型,跟在class或typename后。
[C++] 模版初階-CSDN博客
非類型模板參數(shù)
非類型模板參數(shù),就是用一個常量作為類(函數(shù))模板的一個參數(shù),在類(函數(shù))模板中可將該參數(shù)當(dāng)成常量來使用,定義方法如下:
template<class T, size_t N = 10>
注意:
- 非類型模板參數(shù)只能是整型。
- 浮點(diǎn)數(shù)、類對象以及字符串是不允許作為非類型模板參數(shù)的。
- 非類型的模板參數(shù)必須在編譯期就能確認(rèn)結(jié)果(原因看下文)。
代碼示例
template<int N>
class Array {
public:
int arr[N];
// 其他方法
};
Array<5> myArray; // 創(chuàng)建一個包含5個元素的數(shù)組對象

模板的特化
為什么要有模板的特化
模板技術(shù)提供了強(qiáng)大的泛型編程能力,使得我們能夠編寫與數(shù)據(jù)類型無關(guān)的代碼,從而提高代碼的復(fù)用性和靈活性。然而,在實(shí)際應(yīng)用中,有時需要對特定類型進(jìn)行特殊處理,這時就需要用到模板特化。
**注意:**一般情況下如果函數(shù)模板遇到不能處理或者處理有誤的類型,為了實(shí)現(xiàn)簡單通常都是將該函數(shù)直接給出
模板特化的出現(xiàn)是為了解決模板在處理某些特殊類型時可能遇到的問題。例如,一個通用的比較函數(shù)模板可以比較大多數(shù)類型的數(shù)據(jù),但在遇到指針時,僅比較指針的地址而不是指向的內(nèi)容,這就可能導(dǎo)致錯誤的結(jié)果。模板特化允許為特定類型提供定制的實(shí)現(xiàn),以解決這些特殊情況下的需求。
// 例如日期類中的函數(shù)模板的使用,在使用指針比較的時候就會出現(xiàn)錯誤,這時候就需要進(jìn)行模板特化
template<class T>
bool Less(T left, T right)
{
return left < right;
}
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比較,結(jié)果正確
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比較,結(jié)果錯誤
當(dāng)使用指針進(jìn)行比較的時候比較的就是指針指向的地址,而地址是從棧上向下申請,所以不會按照原本日期類希望的排序方法進(jìn)行排序。
此時,就需要對模板進(jìn)行特化。即:在原模板類的基礎(chǔ)上,針對特殊類型所進(jìn)行特殊化的實(shí)現(xiàn)方式。模板特化中分為函數(shù)模板特化與類模板特化
函數(shù)模板特化
函數(shù)模板特化用于為特定類型定制函數(shù)實(shí)現(xiàn)。它的典型用處是在普通模板無法滿足某些類型需求時提供特定的功能。特化函數(shù)的簽名必須與原模板函數(shù)完全一致。
函數(shù)模板的特化步驟:
- 必須要先有一個基礎(chǔ)的函數(shù)模板;
- 關(guān)鍵字
template后面接一對空的尖括號<>;- 函數(shù)名后跟一對尖括號,尖括號中指定需要特化的類型;
- 函數(shù)形參表: 必須要和模板函數(shù)的基礎(chǔ)參數(shù)類型完全相同,如果不同編譯器可能會報一些奇
怪的錯誤。
使用場景與示例
緊接上面的錯誤案例:假設(shè)我們有一個通用的比較函數(shù)模板Less,它比較兩個對象的大小
template<typename T>
bool Less(T left, T right) {
return left < right;
}
上述模板在大多數(shù)情況下都能正常工作,但如果傳入的是指針類型,那么比較的將是指針的地址而非指向的對象。為了正確比較指針指向的內(nèi)容,我們需要對指針類型進(jìn)行特化:
template<>
bool Less<Date*>(Date* left, Date* right) {
return *left < *right;
}
此特化版本用于比較指針指向的Date對象,確保比較邏輯正確。
函數(shù)模板特化的實(shí)現(xiàn)細(xì)節(jié)
在實(shí)現(xiàn)函數(shù)模板特化時,需要注意以下幾點(diǎn):
- 特化聲明:模板特化的聲明需要緊隨
template<>,然后是函數(shù)簽名,特化的類型需要放在尖括號中。 - 參數(shù)一致性:特化函數(shù)的參數(shù)列表必須與原模板函數(shù)保持一致,不能增加或減少參數(shù),也不能更改參數(shù)的順序或類型。
**注意:**推薦直接寫一個函數(shù)實(shí)現(xiàn)特殊處理,編譯器在處理的時候會優(yōu)先調(diào)用更匹配的。
類模板特化
類模板特化比函數(shù)模板特化更加復(fù)雜,主要分為全特化和偏特化。類模板特化的主要作用是為特定類型提供定制的類定義和實(shí)現(xiàn)。
全特化
全特化是指將模板參數(shù)列表中的所有參數(shù)都具體化(全特化版本中的所有參數(shù)都必須指定具體類型)。
示例
假設(shè)我們有一個通用的數(shù)據(jù)存儲類模板Data,它可以存儲兩個不同類型的對象:
template<typename T1, typename T2>
class Data {
public:
Data() { std::cout << "Data<T1, T2>" << std::endl; }
private:
T1 _d1;
T2 _d2;
};
我們可以為特定的類型組合如int和char進(jìn)行全特化:
template<>
class Data<int, char> {
public:
Data() { std::cout << "Data<int, char>" << std::endl; }
private:
int _d1;
char _d2;
};
在這個全特化版本中,我們?yōu)?code>Data類提供了一個int和一個char類型的特化實(shí)現(xiàn)。這意味著當(dāng)我們創(chuàng)建Data<int, char>類型的對象時,將調(diào)用特化版本的構(gòu)造函數(shù)。
偏特化
偏特化是部分特化的形式,可以僅對部分模板參數(shù)進(jìn)行特化。偏特化比全特化更靈活,允許特化的同時保留一些模板參數(shù)。
偏特化中有兩種表現(xiàn)方式:部分特化、通過限制參數(shù)進(jìn)行特化
部分優(yōu)化
部分特化允許開發(fā)者針對特定的模板參數(shù)進(jìn)行特化,而其他模板參數(shù)保持泛型(需要在template中聲明)。這樣可以在不影響通用模板行為的情況下,為某些特定類型或類型組合提供專門的實(shí)現(xiàn)。
示例:
template<typename T1, typename T2>
class Pair {
public:
Pair(T1 first, T2 second) : first_(first), second_(second) {}
T1 first() const { return first_; }
T2 second() const { return second_; }
private:
T1 first_;
T2 second_;
};
上述模板類是通用的,可以存儲任意類型的兩個數(shù)據(jù)。然而,如果我們需要對第一類型是int的情況進(jìn)行特化,可以使用部分特化:
template<typename T2>
class Pair<int, T2> {
public:
Pair(int first, T2 second) : first_(first), second_(second) {}
int first() const { return first_; }
T2 second() const { return second_; }
void setFirst(int value) { first_ = value; } // 額外的特化方法
private:
int first_;
T2 second_;
};
在這個部分特化版本中,我們特化了Pair模板的第一個類型為int,第二個類型保持泛型。這樣,當(dāng)Pair<int, T2>的對象創(chuàng)建時,將調(diào)用這個特化版本,而不是通用版本。
通過進(jìn)一步限制模板參數(shù)進(jìn)行特化
偏特化為指針類型示例:
當(dāng)需要模板參數(shù)為指針類型的時候,可以對其進(jìn)行特化,以實(shí)現(xiàn)針對于指針的特定邏輯。這在需要對指針執(zhí)行特定操作(如解引用、比較等)時尤為有用。
// 兩個參數(shù)偏特化為指針類型
template <typename T1, typename T2>
class Data<T1*, T2*> {
public:
Data() { std::cout << "Data<T1*, T2*>" << std::endl; }
private:
T1 _d1;
T2 _d2;
};
- 模板特化:
Data<T1*, T2*>,這個偏特化版本對模板的兩個參數(shù)T1和T2進(jìn)行了特化,使得它們必須是指針類型。 - 實(shí)現(xiàn)細(xì)節(jié):在構(gòu)造函數(shù)中打印了一條消息,標(biāo)識這是指針特化的版本。
- 成員變量:特化類中的成員變量依然是
T1和T2類型,不過它們實(shí)際上是指針指向的對象的類型。
偏特化為引用類型示例:
對于引用類型的參數(shù),我們可以通過特化來處理那些需要傳遞引用的情況。這在需要修改外部對象或避免對象復(fù)制時非常有用。
// 兩個參數(shù)偏特化為引用類型
template <typename T1, typename T2>
class Data<T1&, T2&> {
public:
Data(const T1& d1, const T2& d2)
: _d1(d1), _d2(d2) {
std::cout << "Data<T1&, T2&>" << std::endl;
}
private:
const T1& _d1;
const T2& _d2;
};
- 模板特化:
Data<T1&, T2&>,這個偏特化版本對模板的兩個參數(shù)T1和T2進(jìn)行了特化,使得它們必須是引用類型。 - 實(shí)現(xiàn)細(xì)節(jié):在構(gòu)造函數(shù)中接受了
T1和T2類型的引用,并初始化類的成員變量。 - 成員變量:特化類中的成員變量是對傳入對象的常量引用
const T1&和const T2&,這確保了數(shù)據(jù)不會被意外修改。
特化測試結(jié)果分析
void test2() {
Data<double, int> d1; // 調(diào)用全特化的模板
Data<int, double> d2; // 調(diào)用基礎(chǔ)的模板
Data<int*, int*> d3; // 調(diào)用特化的指針版本
Data<int&, int&> d4(1, 2); // 調(diào)用特化的引用版本
}
Data<double, int> d1;:調(diào)用了全特化模板,因?yàn)閰?shù)類型既不是指針也不是引用。Data<int, double> d2;:同樣調(diào)用全特化模板。Data<int*, int*> d3;:調(diào)用了特化的指針版本,因?yàn)閮蓚€參數(shù)都是指針類型。Data<int&, int&> d4(1, 2);:調(diào)用了特化的引用版本,因?yàn)閮蓚€參數(shù)是引用類型(注意,這里初始化引用類型參數(shù)時傳遞的是常量1和2,這些字面量會被隱式轉(zhuǎn)換為合適的引用類型)。

模板特化中的注意事項(xiàng)
實(shí)例化時嚴(yán)格的匹配性
模板編程中,模板實(shí)例化時的匹配性要求非常嚴(yán)格,即使已經(jīng)對模板進(jìn)行了特化,在實(shí)例化時也必須精確匹配到最合適的模板版本。這種嚴(yán)格的匹配性體現(xiàn)在以下幾個方面:
- 全特化:指的是為特定類型組合提供一個完全定制化的實(shí)現(xiàn)。全特化要求在實(shí)例化時完全匹配所有模板參數(shù)類型,只有在參數(shù)完全匹配時,才會使用該特化版本。
- 偏特化:允許對部分模板參數(shù)進(jìn)行特化,同時保持其他參數(shù)的泛型性。在實(shí)例化時,編譯器會優(yōu)先選擇最匹配的特化版本。如果沒有找到完全匹配的特化版本,編譯器才會退而求其次,選擇更加通用的版本。
- 模板匹配順序:編譯器在選擇模板實(shí)例化時,會按照以下優(yōu)先順序進(jìn)行匹配:
- 完全匹配的全特化(優(yōu)先級最高)
- 最匹配的偏特化
- 最通用的模板
指針特化時const的修飾問題
為什么在參數(shù)列表使用const?
防止修改傳入的參數(shù):特化版本中的
Date* const& left和Date* const& right,通過使用const,函數(shù)保證不會修改傳入的指針變量本身的值,即指針的指向保持不變。這是一種安全措施,避免函數(shù)對外部數(shù)據(jù)的不必要的修改。
const與指針修飾關(guān)系
指針本身是常量 (const在*之后)
當(dāng)const放在指針符號*之后時,它修飾的是指針本身,這意味著指針的值(即它指向的內(nèi)存地址)不能被改變。但指針指向的對象的內(nèi)容可以改變。
Date* const pDate;
在這個例子中,pDate是一個常量指針,它指向一個Date類型的對象。pDate本身不能指向別處,但是pDate指向的Date對象的內(nèi)容是可以修改的。
通過特化時將
**const**放在在*****之后即可解決在特化中的修飾關(guān)系。
指向的內(nèi)容是常量 (const在*前面)
當(dāng)const放在*前面時,它修飾的是指針指向的對象,這意味著不能通過這個指針修改指向的對象的內(nèi)容,但指針本身可以指向不同的對象。
const Date* pDate;
在這個例子中,pDate是一個指向Date對象的指針。雖然pDate本身可以指向不同的Date對象,但不能通過pDate來修改它所指向的對象的內(nèi)容。
指針特化時const修飾的應(yīng)用
通用函數(shù)模板
template<class T>
bool LessFunc(const T& left, const T& right)
{
return left < right;
}
該函數(shù)模板中的const修飾的是傳入的left和right不會被改變。
特化函數(shù)模板
template<>
bool LessFunc<Date*>(Date* const& left, Date* const& right)
{
return *left < *right;
}
**Date* const& left**和**Date* const& right**:這兩個參數(shù)都是指向Date對象的常量指針的引用。這意味著:
- 指針本身不可改變:函數(shù)內(nèi)部不能改變
left和right指向的地址(與通用模板中的修飾目的相同)。
為了保持與通用模板中const效果相同,因此寫為Date* const& left。通用模板是為了是傳入的數(shù)據(jù)不被修改,而對于傳入的指針來說,**const**放在*****之后,表示指針本身是常量。換句話說,指針本身的地址不能改變,也就是說,一旦初始化后,指針不能指向其他地址,也就是傳入的指針不能被修改了,和通用模板實(shí)現(xiàn)的效果相同。
因此,Date* const& 的意思是“指向Date對象的常量指針的引用”。這個引用在函數(shù)內(nèi)不會改變其所引用的指針對象,也不能通過引用修改指針本身的指向。
已經(jīng)特化的類中T表示為什么?
在已經(jīng)特化過的類中,不管特化時是將原類型特化為指針類型或者引用類型之類的,在類中使用T的時候一律會按照原類型進(jìn)行使用,也就是說如果在類中要用原類型的指針類型的話,還是需要用T*。
如此表示在const修飾傳參時也有用處,例如上文所理解的LessFunc<Date*>(Date* const& left, Date* const& right),如果特化為指針的類中Date實(shí)際表示為Date*的話,那么在修飾的時候究竟要如何修飾呢。此時就會產(chǎn)生語法與習(xí)慣上的矛盾,所以將T直接作為原類型使用會更加方便與順手。

模板的分離編譯
分離編譯模式簡介
分離編譯是軟件工程中的一個基本概念,它指的是將源代碼分割成多個模塊,每個模塊獨(dú)立編譯,最后通過鏈接器將這些模塊組合成最終的可執(zhí)行文件。這種方式提高了編譯的并行性,同時也使得代碼維護(hù)更加簡單,因?yàn)樾薷囊粋€模塊通常不會影響到其他模塊的編譯。
模板的分離編譯
分離編譯測試
我們有一個模板函數(shù)Add,它的聲明和定義被分別放在不同的文件中:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
在main.cpp中,我們使用了Add函數(shù)的兩個實(shí)例:
#include "a.h"
int main()
{
Add(1, 2); // 整數(shù)實(shí)例
Add(1.0, 2.0); // 浮點(diǎn)數(shù)實(shí)例
return 0;
}
此時當(dāng)運(yùn)行的時候會出現(xiàn)鏈接錯誤。
原因解析
C/C++程序的編譯鏈接原理
C/C++程序的構(gòu)建過程通常分為四個階段:預(yù)處理、編譯、匯編和鏈接。
- 預(yù)處理:預(yù)處理器處理
#include指令和其他預(yù)處理器指令,將頭文件的內(nèi)容插入到源文件中,同時處理宏定義等。 - 編譯:編譯器將預(yù)處理后的源代碼轉(zhuǎn)換成匯編代碼。在這個階段,編譯器檢查語法、詞法和語義錯誤,并且如果一切正確,將代碼轉(zhuǎn)換成機(jī)器可以理解的指令集。
- 匯編:將匯編代碼轉(zhuǎn)換為機(jī)器代碼的二進(jìn)制形式。
- 鏈接:鏈接器將多個目標(biāo)文件(.obj)和庫文件鏈接起來,解決符號引用問題,生成最終的可執(zhí)行文件。

為什么不能分離定義?
**原因:**模板實(shí)例化的代碼并不是編譯的時候在模板位置直接生成的,而是在需要實(shí)例化的時候才會生成特定的具體代碼。
- 實(shí)例化時機(jī):模板的實(shí)例化發(fā)生在編譯器遇到模板函數(shù)或類的使用時。如果模板的定義不在編譯器當(dāng)前正在處理的編譯單元中,那么編譯器無法知道如何實(shí)例化模板,因此不會生成相應(yīng)的函數(shù)代碼。
- 地址問題:如你提到的例子,當(dāng)在
a.cpp中沒有Add模板的具體實(shí)例化代碼時,編譯器不會生成對應(yīng)的函數(shù)。而在main.obj中嘗試使用Add<int>和Add<double>時,鏈接器會在鏈接階段尋找這些函數(shù)的地址,但因?yàn)樗鼈冊诰幾g時沒有被生成,所以鏈接器找不到這些地址,導(dǎo)致鏈接錯誤。- 單定義規(guī)則(One Definition Rule,ODR):C++的單定義規(guī)則要求每個非內(nèi)聯(lián)函數(shù)或變量在一個程序中只能有一個定義。模板的每次實(shí)例化都被視為一個獨(dú)立的函數(shù)或類型定義,這意味著每次實(shí)例化都必須在同一個編譯單元中完成,否則可能會違反ODR。
- **推薦做法:**將模板的聲明和定義放在同一個頭文件中,確保在任何包含該頭文件的編譯單元中都可以進(jìn)行正確的實(shí)例化。


本文來自博客園,作者:DevKevin,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/kevinbee/p/18678238

浙公網(wǎng)安備 33010602011771號