一、非類型模板參數(shù)
模板參數(shù)分為類型形參與非類型形參,其中,類型形參即出現(xiàn)在模板參數(shù)列表中,跟在class或者typename之類的參數(shù)類型名稱,非類型形參,就是用一個常量作為類(函數(shù))模板的一個參數(shù),在類(函數(shù))模板中可將該參數(shù)當(dāng)成常量來使用
我們以定義一個靜態(tài)的數(shù)組為例,在沒有非類型模板參數(shù)的時候,我們只能采用如下的方式來定義一個靜態(tài)的數(shù)組:
#define N 10
template<class T>
class arr
{
public:
// ...
private:
T _a[N];
};
void test()
{
arr<int> arr;
}
但是這樣定義一個數(shù)組有一個極大的缺陷,那就是當(dāng)我們同時需要使用不同大小的數(shù)組的時候,就無法實(shí)現(xiàn),針對這個問題,C++設(shè)計出了非類型模板參數(shù)來解決這個問題,非類型模板參數(shù)和類型模板參數(shù)一樣,也是通過傳遞不同的非類型模板參數(shù)來定義不同的類,代碼如下:
template<class T,size_t N>
class arr
{
public:
// ...
private:
T _a[N];
};
void test()
{
arr<int, 10> arr1;
arr<int, 100> arr2;
}
【注意】
1.非類型模板參數(shù)可以給缺省值
2.浮點(diǎn)數(shù)、類對象以及字符串是不允許作為非類型模板參數(shù)的,非類型模板參數(shù)只能是整形
3.非類型的模板參數(shù)必須在編譯期就能確認(rèn)結(jié)果,即非類型模板參數(shù)的實(shí)參只能是一個常量
二、模板的特化
1.模板特化的概念
通常情況下,使用模板可以實(shí)現(xiàn)一些與類型無關(guān)的代碼,但對于一些特殊類型的可能會得到一些錯誤的結(jié)果,需要特殊處理,比如:實(shí)現(xiàn)了一個專門用來進(jìn)行小于比較的函數(shù)模板
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
// 函數(shù)模板 -- 參數(shù)匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比較,結(jié)果正確
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é)果錯誤
return 0;
}
我們可以看到,Less在絕大多數(shù)的情況下都可以進(jìn)行正常的比較,但是在特定的場景下就可能得到錯誤的結(jié)果,比如在上訴案例中p1指向的d1明顯小于p2指向的d2對象,但是Less內(nèi)部并沒有比較p1和p2指向?qū)ο蟮膬?nèi)容,而比較的是p1和p2的地址,從而使得得到的不是我們預(yù)期的結(jié)果
為了解決上面的問題,我們就需要對模板進(jìn)行特化,即在原模板類的基礎(chǔ)上,針對特殊類型進(jìn)行特殊化處理,模板特化中分為函數(shù)模板特化和類模板特化
2.函數(shù)模板的特化
函數(shù)模板的特化步驟:
1.必須要先有一個基礎(chǔ)的函數(shù)模板
2.關(guān)鍵字template后面接一對空的尖括號<>
3.函數(shù)名后跟一對尖括號,尖括號中指定需要特化的類型
4.函數(shù)形參表: 必須要和模板函數(shù)的基礎(chǔ)參數(shù)類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤
// 函數(shù)模板 -- 參數(shù)匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 對Less函數(shù)模板進(jìn)行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
我們可能會說,我們?yōu)槭裁床恢苯又剌d一個Date*的函數(shù)呢,而是大費(fèi)周章采用模板特化呢,確實(shí)是這樣的,因?yàn)楹瘮?shù)支持多個重載函數(shù),所以注意:一般情況下如果函數(shù)模板遇到不能處理或者處理有誤的類型,為了實(shí)現(xiàn)簡單通常都是將該函數(shù)直接給出
bool Less(Date* left, Date* right)
{
return *left < *right;
}
函數(shù)重載實(shí)現(xiàn)簡單明了,代碼的可讀性高,容易書寫,因?yàn)閷τ谝恍﹨?shù)類型復(fù)雜的函數(shù)模板,特化時特別給出,因此函數(shù)模板不建議特化
3.類模板的特化
3.1 全特化
全特化即是將模板參數(shù)列表中所有的參數(shù)都確定化。
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _d1;
char _d2;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;
}
3.2 偏特化
偏特化:任何針對模版參數(shù)進(jìn)一步進(jìn)行條件限制設(shè)計的特化版本。比如對于以下模板類
template<class T1, class T2>
class Data
{
public:
Data()
{
cout<<"Data<T1, T2>" <<endl;
}
private:
T1 _d1;
T2 _d2;
};
偏特化有以下兩種表現(xiàn)方式:
1.部分特化
將模板參數(shù)類表中的一部分參數(shù)特化
// 將第二個參數(shù)特化為int
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout<<"Data<T1, int>" <<endl;
}
private:
T1 _d1;
int _d2;
};
void TestVector()
{
// 第二個參數(shù)與模板特化中的特化參數(shù)相同,優(yōu)先使用特化模板進(jìn)行實(shí)例化
Data<int, int> d1;
// 使用普通模板實(shí)例化
Data<int, char> d2;
}
我們可以看到,我們將模板的部分參數(shù)顯示指定為某種具體的類型,這樣模板參數(shù)進(jìn)行匹配的時候會優(yōu)先匹配
2.參數(shù)更進(jìn)一步的限制
偏特化并不僅僅是指特化部分參數(shù),而是針對模板參數(shù)更進(jìn)一步的條件限制所設(shè)計出來的一個特化版本
//兩個參數(shù)偏特化為指針類型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//兩個參數(shù)偏特化為引用類型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
void test()
{
Data<double, int> d1; // 調(diào)用特化的int版本
Data<int, double> d2; // 調(diào)用基礎(chǔ)的模板
Data<int*, int*> d3; // 調(diào)用特化的指針版本
Data<int&, int&> d4(1, 2); // 調(diào)用特化的指針版本
}
從上面的結(jié)果,我們可以看到,通過偏特化對模板參數(shù)進(jìn)行進(jìn)一步限制,比如將模板參數(shù)定義為<T*,T*>,這樣不管是任何類型的指針都會調(diào)用該特化模板,從而實(shí)現(xiàn)了在限制參數(shù)類型的同時又不會將參數(shù)局限為某一具體類型
4.類模板特化應(yīng)用示例
有如下專門用來按照小于比較的類模板Less:
#include<vector>
#include <algorithm>
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
void test()
{
Date d1(2023, 4, 3);
Date d2(2023, 4, 1);
Date d3(2023, 4, 2);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,結(jié)果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
for (auto e : v1)
{
cout << e << endl;
}
cout << endl;
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,結(jié)果錯誤日期還不是升序,而v2中放的地址是升序
// 此處需要在排序過程中,讓sort比較v2中存放地址指向的日期對象
// 但是走Less模板,sort在排序時實(shí)際比較的是v2中指針的地址,因此無法達(dá)到預(yù)期
sort(v2.begin(), v2.end(), Less<Date*>());
for (auto e : v2)
{
cout << *e << endl;
}
cout << endl;
}
通過觀察上述程序的結(jié)果發(fā)現(xiàn),對于日期對象可以直接排序,并且結(jié)果是正確的。但是如果待排序元素是指針,結(jié)果就不一定正確。因?yàn)椋簊ort最終按照Less模板中方式比較,所以只會比較指針,而不是比較指針指向空間中內(nèi)容,此時可以使用類版本特化來處理上述問題:
// 對Less類模板按照指針方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
三、模板的分離編譯
1.什么是分離編譯
一個程序(項目)由若干個源文件共同實(shí)現(xiàn),而每個源文件單獨(dú)編譯生成目標(biāo)文件,最后將所有目標(biāo)文件鏈接起來形成單一的可執(zhí)行文件的過程稱為分離編譯模式
2.模板的分離編譯
我們以stack為例:
stack.h
#pragma once
#include<iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
stack.cpp
#include "Stack.h"
template<class T>
Stack<T>::Stack(int capacity)
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
template<class T>
Stack<T>::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
template<class T>
void Stack<T>::Push(const T& x)
{
// ....
//
_a[_top++] = x;
}
test.cpp
#include "stack.h"
int main()
{
Stack<int> st;
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
當(dāng)我們運(yùn)行的時候發(fā)現(xiàn)出現(xiàn)了鏈接性錯誤;造成這種的原因如下:
我們知道,一個.c/.cpp程序變成.exe可執(zhí)行程序一共需要經(jīng)歷四個步驟,分別是預(yù)處理,編譯,匯編和鏈接,在這個過程中,他們所執(zhí)行的工作為:
預(yù)處理:注釋的刪除,#define定義的符號,宏的替換以及刪除,各種條件編譯的處理,頭文件的展開
編譯:進(jìn)行詞法分析,語法分析,語義分析和符號匯總
匯編:生成符號表
鏈接:合并段表,符號表的合并和重定位
此外,預(yù)處理,編譯,匯編這幾個階段每個源(.c文件)文件都是獨(dú)立進(jìn)行的,只有在鏈接的時候才會將這幾個目標(biāo)文件合并到一起形成可執(zhí)行程序
綜上所訴,我們可知程序報錯的原因如下:
1.預(yù)處理時,stack.h頭文件分別在stack.c和test.c源文件展開
2.經(jīng)過編譯,stack.cpp和test.cpp分別轉(zhuǎn)變成匯編代碼
3.經(jīng)過匯編,stack.cpp里面有stack,push函數(shù)的聲明,但是沒有他們的定義,所以test.cpp在生成符號表的時候會給這些函數(shù)一個無效的地址,此外,由于stack.cpp里面沒有對函數(shù)模板實(shí)例化的代碼,即沒有stack,也就沒有生成具體的代碼,即沒有stack的定義,所以stack.cpp的符號表里函數(shù)對應(yīng)的地址是一個無效的地址
4.在鏈接時,需要將test.cpp和stack.cpp符號表的內(nèi)容進(jìn)行合并與重定位,但由于他們符號表中都是無效的地址,所以會發(fā)生鏈接錯誤
我們了解程序報錯的原因之后,想到的第一個解決方案應(yīng)該是在stack.cpp中對模板進(jìn)行實(shí)例化
// 在stack.cpp中增加對stack的顯式實(shí)例化
template class Stack<int>;
這樣的做法確實(shí)能夠解決這個問題,但是當(dāng)我們定義一個存放double類型的棧的時候,此時,我們又需要在stack.cpp中對stack再進(jìn)行實(shí)例化一次,如果我們再定義不同的對象,就需要不斷的進(jìn)行顯式實(shí)例化,也就是說,在同一份代碼中我們只能定義同一類類型的對象,那么這樣就十分麻煩,也失去了模板的初衷了,所以模板不支持分離編譯,我們一般采用如下的方法解決:
1.將聲明和定義放到一個文件"xxx.hpp" 里面或者xxx.h**其實(shí)也是可以的
2.模板定義的位置顯式實(shí)例化。這種方法不實(shí)用,不推薦使用
【分離編譯擴(kuò)展閱讀】為什么C++編譯器不能支持對模板的分離式編譯
但是聲明解決的兩種方式有一個問題,就是將函數(shù)的聲明和定義放在同一個文件中,我們就將類提供給別人使用時,也將底層實(shí)現(xiàn)也暴露了給別人
四、模板總結(jié)
模板優(yōu)點(diǎn)
1.模板復(fù)用了代碼,節(jié)省資源,更快的迭代開發(fā),C++的標(biāo)準(zhǔn)模板庫(STL)因此而產(chǎn)生
2.增強(qiáng)了代碼的靈活性
模板缺點(diǎn)
1.模板會導(dǎo)致代碼膨脹問題,也會導(dǎo)致編譯時間變長文章來源:http://www.zghlxwxcb.cn/news/detail-422678.html
2.出現(xiàn)模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤文章來源地址http://www.zghlxwxcb.cn/news/detail-422678.html
到了這里,關(guān)于【C++】模板進(jìn)階--非類型模板參數(shù)&&模板特化及分離編譯的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!