文章目錄
- 非類型模板參數
- 函數模板的特化
- 類模板的特化
- 全特化
- 偏特化
- 部分參數特化
- 參數修飾特化
- 模板分離編譯
- 解決方法
非類型模板參數
模板的參數分為兩種:
- 類型參數: 則是我們通常使用的方式,就是在模板的參數列表中在
class
后面加上參數的類型名稱。 - 非類型參數: 則是用一個常量作為模板的參數,在模板中可以當作常量來使用,通常是需要指明大小或者初始化內容的才會用到。
類型參數我們在上一篇博客中講的很詳細了,不再贅述。而非類型參數比較常見的就是 c++
中的 array
:
array
的底層就是直接使用的數組,而數組創建時必須指明大小,并且大小得是個常量,所以就會用到非類型模板參數。
注意:
- 浮點數、自定義類型、類對象以及字符串是不允許作為非類型模板參數的。
- 非類型參數必須在編譯期就能確認結果。
通常情況下非類型模板參數都是使用字符型和整型。
函數模板的特化
當我們使用模板來實現一個函數,肯定是想利用它來解決邏輯相同但數據類型不同的一些問題,來實現代碼的復用,但是也存在某些特例,比如針對某一情景或者某一類型,這個模板需要有特殊的處理,這個時候就需要用到模板的特化。
例如:
template<class T>
bool IsEqual(T str1, T str2)
{return str1 == str2;
}int main()
{char str1[] = "hello";char str2[] = "hello";if (IsEqual(str1, str2))cout << "true";elsecout << "false";
}
這里不同的原因是傳遞過去的是兩個 char*
類型,他們兩個比較的不是字符串的內容,而是指針的地址,這里 str1、str2
是在棧上開辟一塊空間后再將 hello
拷貝過去,而 IsEqual
比較的是兩者指向的兩塊不同內存(也就是兩個 hello
的內存)的首地址,不可能相同。
如果要比較 char*
,就得用到 strcmp
來對這個情況進行特殊處理, 也就是模板的特化。
template<>
bool IsEqual<char*>(char* str1, char* str2)
{return strcmp(str1, str2) == 0;
}
// extern int strcmp(const char *s1,const char *s2);
函數模板的特化步驟:
- 必須要先有一個基礎的函數模板
- 關鍵字
template
后面接一對空的尖括號 <>
- 函數名后面的
<>
中指定需要特化的類型 - 特化的函數其形參 一定要與 模板的形參 類型完全相同。
類模板的特化
類也是同理,如果需要有特殊情景也需要特化處理:
類模板如下:
template<class T1, class T2>
class test
{
public:test(){cout << "test<T1, T2>" << endl;}private:T1 _x;T2 _y;
};
全特化
全特化即是將模板參數列表中所有的參數都確定化。
這里對 test<int,double>
版本特化。
template<>
class test<int, double>
{
public:test(){cout << "test<int, double>" << endl;}private:int _x;double _y;
};int main()
{test<double, double> t1;test<int, double> t2;
}
偏特化
偏特化即是任何針對模版參數進一步進行條件限制設計的特化版本。
偏特化有兩種表現方式:一種是部分參數特化,一種是參數修飾特化。
部分參數特化
這里對第二個參數特化,只要第二個參數是 double
就會調用對應特化版本。
template<class T1>
class test<T1, double>
{
public:test(){cout << "test<T1, double>" << endl;}private:T1 _x;double _y;
};int main()
{test<double, double> t1;test<int, double> t2;test<float, double> t3;test<double, int> t4;
}
參數修飾特化
比如用指針或者引用來修飾類型,也可以進行特化。
template<class T1, class T2>
class test<T1*, T2*>
{
public:test(){cout << "test<T1*, T2*>" << endl;}private:T1* _x;T2* _y;
};int main()
{test<double, int> t1;test<int*, double*> t2;test<float*, double*> t3;test<char*, double> t4;
}
模板分離編譯
對于一個代碼量比較多的項目,通常都會采用聲明與定義分離的方法,比如在頭文件進行聲明,在源文件完成代碼的實現,最后通過鏈接的方法鏈接成單一的可執行文件。但是 C++
的編譯器卻不支持模板的分離編譯,一旦進行分離編譯,就會出現鏈接錯誤。
//頭文件a.h
template<class T>
bool IsEqual(const T& str1, const T& str2);-------------
//源文件a.cpp
template<class T>
bool IsEqual(const T& str1, const T& str2)
{return str1 == str2;
}
--------------
//test.c
#include<iostream>
#include"a.h"
using namespace std;int main()
{cout << IsEqual(3, 5);cout << IsEqual('a', 'b');
}
這里看上去是沒有問題的,但是涉及到了模板的實例化規則。
首先,一個編譯單元是指一個 .cpp
文件以及它所 #include
的所有 .h
文件,.h
文件里的代碼將會被擴展到包含它的 .cpp
文件里,然后編譯器編譯該 .cpp
文件為一個 .obj
文件(假定我們的平臺是 win32
,Linux
則是 .o
文件),后者擁有 PE(Portable Executable,即windows可執行文件)
文件格式,并且本身包含的就已經是二進制碼,但是不一定能夠執行,因為并不保證其中一定有 main函數
。當編譯器將一個工程里的所有 .cpp
文件以分離的方式編譯完畢后,再由連接器(linker)
進行連接成為一個 .exe
(Linux
為 .out
)文件。
那么來看上面的代碼,
test.c
和a.cpp
被編譯器編譯成test.obj
和a.obj
。- 當編譯器處理主函數調用
IsEqual
的時,這是第一次使用模板,會進行實例化,而實例化需要模板的定義,但是在test.c
中并沒有模板的定義,雖然在test.obj
文件中頭文件a.h
也被展開,但可惜的是a.h
中只有模板的聲明 。 - 那么編譯器只能寄希望于連接器,希望它能夠在其他
.obj
里面找到IsEqual
的實例,本題中連接器就是去a.obj
中找實例,但問題在于,雖然a.cpp
中有模板的定義,但并沒有使用過這個模板,因此a.obj
中不存在IsEqual
的實例。因此連接器只能返回一個連接錯誤。
解決方法
這個問題其實沒有什么完美的解決方法
- 將聲明和定義放到同一個頭文件中。(導致頭文件代碼過于龐大)
- 模板定義的位置顯式實例化。(不實用)
這個問題劉未鵬大佬寫的非常好,可以學習一下他的博客
為什么C++編譯器不能支持對模板的分離式編譯