一执泰、為什么要有函數(shù)模板
在泛型編程出現(xiàn)前典蝌,我們要實(shí)現(xiàn)一個(gè)swap函數(shù)得這樣寫(xiě):
void swap(int &a, int &b) {
int tmp{a};
a = b;
b = tmp;
}
但這個(gè)函數(shù)只支持int型的變量交換距潘,如果我們要做float, long, double, std::string等等類型的交換時(shí)罗侯,只能不斷加入新的重載函數(shù)。這樣做不但代碼冗余伏钠,容易出錯(cuò),還不易維護(hù)谨设。C++函數(shù)模板有效解決了這個(gè)問(wèn)題熟掂。函數(shù)模板擺脫了類型的限制,提供了通用的處理過(guò)程扎拣,極大提升了代碼的重用性赴肚。
二、什么是函數(shù)模板
cppreference中給出的定義是"函數(shù)模板定義一族函數(shù)"鹏秋,怎么理解呢尊蚁?我們先來(lái)看一段簡(jiǎn)單的代碼
#include <iostream>
template<typename T>
void swap(T &a, T &b) {
T tmp{a};
a = b;
b = tmp;
}
int main() {
int a = 2, b = 3;
swap(a, b); // 使用函數(shù)模板
std::cout << "a=" << a << ", b=" << b << std::endl;
}
swap支持多種類型的通用交換邏輯。它跟普通C++函數(shù)的區(qū)別在于其函數(shù)聲明(declaration)前面加了個(gè)template<typename T>侣夷,這句話告訴編譯器横朋,swap中(函數(shù)參數(shù)、返回值百拓、函數(shù)體中)出現(xiàn)類型T時(shí)琴锭,不要報(bào)錯(cuò),T是一個(gè)通用類型衙传。
函數(shù)模板的格式:
template<parameter-list> function-declaration
函數(shù)模板在形式上分為兩部分:模板决帖、函數(shù)。在函數(shù)前面加上template<...>就成為函數(shù)模板蓖捶,因此對(duì)函數(shù)的各種修飾(inline地回、constexpr等)需要加在function-declaration上,而不是template前。如
template<typename T>
inline T min(const T &, const T &);
parameter-list是由英文逗號(hào)(,)分隔的列表刻像,每項(xiàng)可以是下列之一:
序號(hào) | 名稱 | 說(shuō)明 |
---|---|---|
1 | 非類型形參 | 已知的數(shù)據(jù)類型畅买,如整數(shù)、指針等细睡,C++11中有三種形式: int N int N = 1: 帶默認(rèn)值谷羞,該值必須是一個(gè)常量或常量表達(dá)式 int ...N: 模板參數(shù)包(可變參數(shù)模板) |
2 | 類型形參 | swap值用的形式,格式為: typename name[ = default] typename ... name: 模板參數(shù)包 |
3 | 模板模板形參 | 沒(méi)錯(cuò)有兩個(gè)"模板"溜徙,這個(gè)比較復(fù)雜湃缎,有興趣的同學(xué)可以參考 cppreference之模板形參與模板實(shí)參 |
上面swap函數(shù)模板,使用了類型形參蠢壹。函數(shù)模板就像是一種契約嗓违,任何滿足該契約的類型都可以做為模板實(shí)參。而契約就是函數(shù)實(shí)現(xiàn)中图贸,模板實(shí)參需要支持的各種操作靠瞎。上面swap中T需要滿足的契約為:支持拷貝構(gòu)造和賦值。
template<typename T>
void swap(T &a, T &b) {
T tmp{a}; // 契約一:T需要支持拷貝構(gòu)造
a = b; // 契約二:T需要支持賦值操作
b = tmp;
}
三求妹、函數(shù)模板不是函數(shù)
剛才我們提到函數(shù)模板用來(lái)定義一族函數(shù)乏盐,而不是一個(gè)函數(shù)。C++是一種強(qiáng)類型的語(yǔ)言制恍,在不知道T的具體類型前父能,無(wú)法確定swap需要占用的棧大小(參數(shù)棧,局部變量)净神,同時(shí)也不知道函數(shù)體中T的各種操作如何實(shí)現(xiàn)何吝,無(wú)法生成具體的函數(shù)。只有當(dāng)用具體類型去替換T時(shí)鹃唯,才會(huì)生成具體函數(shù)爱榕,該過(guò)程叫做函數(shù)模板的實(shí)例化。當(dāng)在main函數(shù)中調(diào)用swap(a,b)
時(shí)坡慌,編譯器推斷出此時(shí)T
為int
黔酥,然后編譯器會(huì)生成int版的swap函數(shù)供調(diào)用。所以相較普通函數(shù)洪橘,函數(shù)模板多了生成具體函數(shù)這一步跪者。如果我們只是編寫(xiě)了函數(shù)模板,但不在任何地方使用它(也不顯式實(shí)例化)熄求,則編譯器不會(huì)為該函數(shù)模板生成任何代碼渣玲。
函數(shù)模板實(shí)例化分為隱式實(shí)例化和顯式實(shí)例化。
3.1 隱式實(shí)例化
仍以swap為例弟晚,我們?cè)趍ain中調(diào)用swap(a,b)
時(shí)忘衍,就發(fā)生了隱式實(shí)例化逾苫。當(dāng)函數(shù)模板被調(diào)用,且在之前沒(méi)有顯式實(shí)例化時(shí)枚钓,即發(fā)生函數(shù)模板的隱式實(shí)例化隶垮。如果模板實(shí)參能從調(diào)用的語(yǔ)境中推導(dǎo),則不需要提供秘噪。
#include <iostream>
template<typename T>
void print(const T &r) {
std::cout << r << std::endl;
}
int main() {
// 隱式實(shí)例化print<int>(int)
print(1);
// 實(shí)例化print<char>(char)
print<>('c');
// 仍然是隱式實(shí)例化,我們希望編譯器生成print<double>(double)
print<double>(1);
}
3.2 顯式實(shí)例化
在函數(shù)模板定義后勉耀,我們可以通過(guò)顯式實(shí)例化的方式告訴編譯器生成指定實(shí)參的函數(shù)指煎。顯式實(shí)例化聲明會(huì)阻止隱式實(shí)例化。
template<typename R, typename T1, typename T2>
R add(T1 a, T2 b) {
return static_cast<R>(a + b);
}
// 顯式實(shí)例化
template double add<double, int, double>(int, double);
// 顯式實(shí)例化, 推導(dǎo)出第三個(gè)模板實(shí)參
template int add<int, int>(int, int);
// 全部由編譯器推導(dǎo)
template double add(double, double);
如果我們?cè)陲@式實(shí)例化時(shí)便斥,只指定部分模板實(shí)參至壤,則指定順序必須自左至右依次指定,不能越過(guò)前參模板形參枢纠,直接指定后面的像街。
四、函數(shù)模板的使用
4.1 使用非類型形參
#include <iostream>
// N必須是編譯時(shí)的常量表達(dá)式
template<typename T, int N>
void printArray(const T (&a)[N]) {
std::cout << "[";
const char *sep = "";
for (int i = 0; i < N; i++, (sep = ", ")) {
std::cout << sep << a[i];
}
std::cout << "]" << std::endl;
}
int main() {
// T: int, N: 3
printArray({1, 2, 3});
}
//輸出:[1, 2, 3]
4.2 返回值為auto
有些時(shí)候我們會(huì)碰到這樣一種情況晋渺,函數(shù)的返回值類型取決于函數(shù)參數(shù)某種運(yùn)算后的類型镰绎。對(duì)于這種情況可以采用auto關(guān)鍵字作為返回值占位符。
template<typename T1, typename T2>
auto multi(T a, T b) -> decltype(a * b) {
return a * b;
}
decltype操作符用于查詢表達(dá)式的數(shù)據(jù)類型木西,也是C++11標(biāo)準(zhǔn)引入的新的運(yùn)算符畴栖,其目的是解決泛型編程中有些類型由模板參數(shù)決定,而難以表示的問(wèn)題八千。為何要將返回值后置呢吗讶?
// 這樣是編譯不過(guò)去的,因?yàn)閐ecltype(a*b)中恋捆,a和b還未聲明照皆,編譯器不知道a和b是什么。
template<typename T1, typename T2>
decltype(a*b) multi(T a, T b) {
return a*+ b;
}
//編譯時(shí)會(huì)產(chǎn)生如下錯(cuò)誤:error: use of undeclared identifier 'a'
4.3 類成員函數(shù)模板
函數(shù)模板可以做為類的成員函數(shù)沸停。
#include <iostream>
class object {
public:
template<typename T>
void print(const char *name, const T &v) {
std::cout << name << ": " << v << std::endl;
}
};
int main() {
object o;
o.print("name", "Crystal");
o.print("age", 18);
}
輸出:
name: Crystal
age: 18
需要注意的是:函數(shù)模板不能用作虛函數(shù)膜毁。這是因?yàn)镃++編譯器在解析類的時(shí)候就要確定虛函數(shù)表(vtable)的大小,如果允許一個(gè)虛函數(shù)是函數(shù)模板愤钾,那么就需要在解析這個(gè)類之前掃描所有的代碼爽茴,找出這個(gè)模板成員函數(shù)的調(diào)用或顯式實(shí)例化操作,然后才能確定虛函數(shù)表的大小绰垂,而顯然這是不可行的室奏。
4.4 函數(shù)模板重載
函數(shù)模板之間、普通函數(shù)和模板函數(shù)之間可以重載劲装。編譯器會(huì)根據(jù)調(diào)用時(shí)提供的函數(shù)參數(shù)胧沫,調(diào)用能夠處理這一類型的最佳匹配版本昌简。在匹配度上,一般按照如下順序考慮:
順序 | 行為 |
---|---|
1 | 最符合函數(shù)名和參數(shù)類型的普通函數(shù) |
2 | 特殊模板(具有非類型形參的模板绒怨,即對(duì)T有類型限制) |
3 | 普通模板(對(duì)T沒(méi)有任何限制的) |
4 | 通過(guò)類型轉(zhuǎn)換進(jìn)行參數(shù)匹配的重載函數(shù) |
#include <iostream>
template<typename T>
const T &max(const T &a, const T &b) {
std::cout << "max(&, &) = ";
return a > b ? a : b;
}
// 函數(shù)模板重載
template<typename T>
const T *max(T *a, T *b) {
std::cout << "max(*, *) = ";
return *a > *b ? a : b;
}
// 函數(shù)模板重載
template<typename T>
const T &max(const T &a, const T &b, const T &c) {
std::cout << "max(&, &, &) = ";
const T &t = (a > b ? a : b);
return t > c ? t : c;
}
// 普通函數(shù)
const char *max(const char *a, const char *b) {
std::cout << "max(const char *, const char *) = ";
return strcmp(a, b) > 0 ? a : b;
}
int main() {
int a = 1, b = 2;
std::cout << max(a, b) << std::endl;
std::cout << *max(&a, &b) << std::endl;
std::cout << max(a, b, 3) << std::endl;
std::cout << max("en", "ch") << std::endl;
// 可以通過(guò)空模板實(shí)參列表來(lái)限定編譯器只匹配函數(shù)模板
std::cout << max<>("en", "ch") << std::endl;
}
輸出
max(&, &) = 2
max(*, *) = 2
max(&, &, &) = 3
max(const char *, const char *) = en
max(*, *) = en
可以通過(guò)空模板實(shí)參列表來(lái)限定編譯器只匹配函數(shù)模板纯赎,比如main函數(shù)中的最后一條語(yǔ)句。
4.5 函數(shù)模板特化
當(dāng)函數(shù)模板需要對(duì)某些類型進(jìn)行特別處理南蹂,這稱為函數(shù)模板的特化犬金。當(dāng)我們定義一個(gè)特化版本時(shí),函數(shù)參數(shù)類型必須與一個(gè)先前聲明的模板中對(duì)應(yīng)的類型匹配六剥。函數(shù)模板特化的本質(zhì)是實(shí)例化一個(gè)模板晚顷,而非重載它。因此疗疟,特化不影響編譯器函數(shù)匹配该默。
template<typename T1, typename T2>
int compare(const T1 &a, const T2 b) {
return a - b;
}
// 對(duì)const char *進(jìn)行特化
template<>
int compare(const char * const &a, const char * const &b) {
return strcmp(a, b);
}
上面的例子中針對(duì)const char *的特化,我們其實(shí)可以通過(guò)函數(shù)重載達(dá)到相同效果策彤。因此對(duì)于函數(shù)模板特化栓袖,目前公認(rèn)的觀點(diǎn)是沒(méi)什么用,并且最好別用店诗。Why Not Specialize Function Templates?
但函數(shù)模板特化和重載在重載決議時(shí)有些細(xì)微的差別裹刮。這些差別中比較有用的一個(gè)是阻止某些隱式轉(zhuǎn)換。如當(dāng)你只有void foo(int)時(shí)庞瘸,以浮點(diǎn)類型調(diào)用會(huì)發(fā)生隱式轉(zhuǎn)換必指,這可以通過(guò)特化來(lái)阻止:
template <class T> void foo(T);
template <> void foo(int) {}
foo(3.0); // link error,阻止float隱式轉(zhuǎn)換為int
雖然模板配重載也可以達(dá)到同樣的效果恕洲,但特化版的意圖更加明確塔橡。
函數(shù)模板及其特化版本應(yīng)該聲明在同一個(gè)頭文件中。所有同名模板的聲明應(yīng)該放在前面霜第,然后是這些模板的特化版本葛家。
五、變參函數(shù)模板(模板參數(shù)包)
這是C++11引入的新特性泌类,用來(lái)表示任意數(shù)量的模板形參癞谒。其語(yǔ)法樣式如下:
template<typename ...Args> // Args: 模板參數(shù)包
void foo(Args ... args); // args: 函數(shù)參數(shù)包
在模板形參Args的左邊出現(xiàn)三個(gè)英文點(diǎn)號(hào)"...",表示Args是零個(gè)或多個(gè)類型的列表刃榨,是一個(gè)模板參數(shù)包(template parameter pack)弹砚。正如其名稱一樣,編譯器會(huì)將Args所表示的類型列表打成一個(gè)包枢希,將其當(dāng)做一個(gè)特殊類型處理桌吃。相應(yīng)的函數(shù)參數(shù)列表中也有一個(gè)函數(shù)參數(shù)包。與普通模板函數(shù)一樣苞轿,編譯器從函數(shù)的實(shí)參推斷模板參數(shù)類型茅诱,與此同時(shí)還會(huì)推斷包中參數(shù)的數(shù)量逗物。
// sizeof...() 是C++11引入的參數(shù)包的操作函數(shù),用來(lái)取參數(shù)的數(shù)量
template<typename ...Args>
int length(Args ... args) {
return sizeof...(Args);
}
// 以下語(yǔ)句將在屏幕打印出:2
std::cout << length(1, "hello") << std::endl;
變參函數(shù)模板主要用來(lái)處理既不知道要處理的實(shí)參的數(shù)目也不知道它們的類型時(shí)的場(chǎng)景瑟俭。既然我們對(duì)實(shí)參數(shù)量以及類型都一無(wú)所知翎卓,那么我們?cè)趺词褂盟?最常用的方法是遞歸。
5.1 遞歸
通過(guò)遞歸來(lái)遍歷所有的實(shí)參摆寄,這需要一點(diǎn)點(diǎn)的技巧失暴,需要給出終止遞歸的條件,否則遞歸將無(wú)限進(jìn)行微饥。
#include <iostream>
// 遞歸終止
void print() { /// 1
std::cout << std::endl;
}
// 打印綁定到t的實(shí)參
template<typename T, typename... Args>
void print(const T &t, const Args &... args) { /// 2
std::cout << t << (sizeof...(args) > 0 ? ", " : "");
// 編譯時(shí)展開(kāi):通過(guò)在args右邊添加省略號(hào)(...)進(jìn)行展開(kāi)逗扒,打印參數(shù)包中剩余的參數(shù)
print(args...);
}
int main() {
print(1, "hello", "C++", 11);
return 0;
}
//輸出: 1, hello, C++, 11
該例子的技巧在于,函數(shù)2提供了const T &t參數(shù)畜号,保證至少有一個(gè)參數(shù),避免了與函數(shù)1在args為0時(shí)的沖突允瞧。需要注意的是简软,遞歸是指編譯器遞歸,不是運(yùn)行過(guò)程時(shí)的遞歸調(diào)用述暂。實(shí)際上編譯器為函數(shù)2生成了4個(gè)重載版本痹升,并依次調(diào)用。下圖是在運(yùn)行時(shí)的調(diào)用棧畦韭,可以看到共有5個(gè)重載版本的print函數(shù)疼蛾,4個(gè)遞歸展開(kāi)的函數(shù)2,外加函數(shù)1艺配。遞歸最終結(jié)束在函數(shù)1處察郁。
5.2 包擴(kuò)展
對(duì)于一個(gè)參數(shù)包,不管是模板參數(shù)包還是函數(shù)參數(shù)包转唉,我們對(duì)它能做的只有兩件事:sizeof...()和包擴(kuò)展皮钠。前面我們說(shuō)過(guò)編譯器將參數(shù)包當(dāng)作一個(gè)類型來(lái)處理,因此使用的時(shí)候需要將其展開(kāi)赠法,展開(kāi)時(shí)我們需要提供用于每個(gè)元素的處理模式(pattern)麦轰。包擴(kuò)展就是對(duì)參數(shù)包中的每一個(gè)元素應(yīng)用模式,獲取得擴(kuò)展后的列表砖织。最簡(jiǎn)單的包擴(kuò)展方式就是我們?cè)谏瞎?jié)中看到的const Args &...
和args...
款侵,該擴(kuò)展是將其擴(kuò)展為構(gòu)成元素。C++11還支持更復(fù)雜的擴(kuò)展模式侧纯,如:
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
template<typename T>
std::string to_str(const T &r) {
std::stringstream ss;
ss << "\"" << r << "\"";
return ss.str();
}
template<typename... Args>
void init_vector(std::vector<std::string> &vec, const Args &...args) {
// 復(fù)雜的包擴(kuò)展方式
vec.assign({to_str(args)...});
}
int main() {
std::vector<std::string> vec;
init_vector(vec, 1, "hello", "world");
std::cout << "vec.size => " << vec.size() << std::endl;
for (auto r: vec) {
std::cout << r << std::endl;
}
}
運(yùn)行程序?qū)a(chǎn)生如下輸出:
vec.size => 3
"1"
"hello"
"world"
擴(kuò)展過(guò)程中模式(pattern)會(huì)獨(dú)立地應(yīng)用于包中的每一個(gè)元素新锈。同時(shí)pattern也可以接受多個(gè)參數(shù),并非僅僅只能接受參數(shù)包眶熬。
5.3 參數(shù)包的轉(zhuǎn)發(fā)
C++11中壕鹉,我們可以同時(shí)使用變參函數(shù)模板和std::forward機(jī)制來(lái)編寫(xiě)函數(shù)剃幌,將實(shí)參原封不動(dòng)地傳遞給其它函數(shù)。其中典型的應(yīng)用是std::vector::emplace_back操作:
template<typename T, typename Allocator>
template <class... _Args>
void vector<T, Allocator>::emplace_back(_Args&&... __args) {
push_back (T(forward<_Args>(__args)... ));
}
六晾浴、其它
6.1 函數(shù)模板 .vs. 模板函數(shù)
函數(shù)模板重點(diǎn)在模板负乡。表示這是一個(gè)模板,用來(lái)生成函數(shù)脊凰。
模板函數(shù)重點(diǎn)在函數(shù)抖棘。表示的是由一個(gè)模板生成而來(lái)的函數(shù)。
6.2 cv限定
cv限定是指函數(shù)參數(shù)中有const狸涌、volatile或mutable限定切省。已指定、推導(dǎo)出或從默認(rèn)模板實(shí)參獲得所有模板實(shí)參時(shí)帕胆,函數(shù)參數(shù)列表中每次模板形參的使用都會(huì)被替換成對(duì)應(yīng)的模板實(shí)參朝捆。替換后:
- 所有數(shù)組類型和函數(shù)類型參數(shù)被調(diào)整成為指針
- 所有頂層cv限定符從函數(shù)參數(shù)被丟棄,如在普通函數(shù)聲明中懒豹。
頂層cv限定符的去除不影響參數(shù)類型的使用芙盘,因?yàn)樗霈F(xiàn)于函數(shù)中:
template <typename T> void f(T t);
template <typename X> void g(const X x);
template <typename Z> void h(Z z, Z *zp);
// 兩個(gè)不同函數(shù)有同一類型,但在函數(shù)中脸秽, t有不同的cv限定
f<int>(1); // 函數(shù)類型是 void(int) 儒老, t 為 int
f<const int>(1); // 函數(shù)類型是 void(int) , t 為 const int
// 二個(gè)不同函數(shù)擁有同一類型和同一 x
// (指向此二函數(shù)的指針不相等记餐,且函數(shù)局域的靜態(tài)變量可以擁有不同地址)
g<int>(1); // 函數(shù)類型是 void(int) 驮樊, x 為 const int
g<const int>(1); // 函數(shù)類型是 void(int) , x 為 const int
// 僅丟棄頂層 cv 限定符:
h<const int>(1, NULL); // 函數(shù)類型是 void(int, const int*)
// z 為 const int 片酝, zp 為 int*
上一篇 C++11多線程-內(nèi)存模型 |
目錄 | 下一篇 C++11多線程-類模板 |
---|