一、為什么要有函數(shù)模板
在泛型編程出現(xiàn)前,我們要實現(xiàn)一個swap函數(shù)得這樣寫:
void swap(int &a, int &b) {
int tmp{a};
a = b;
b = tmp;
}
但這個函數(shù)只支持int型的變量交換,如果我們要做float, long, double, std::string等等類型的交換時,只能不斷加入新的重載函數(shù)。這樣做不但代碼冗余,容易出錯,還不易維護。C++函數(shù)模板有效解決了這個問題。函數(shù)模板擺脫了類型的限制,提供了通用的處理過程,極大提升了代碼的重用性。
二、什么是函數(shù)模板
cppreference中給出的定義是"函數(shù)模板定義一族函數(shù)",怎么理解呢?我們先來看一段簡單的代碼
#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)前面加了個template<typename T>,這句話告訴編譯器,swap中(函數(shù)參數(shù)、返回值、函數(shù)體中)出現(xiàn)類型T時,不要報錯,T是一個通用類型。
函數(shù)模板的格式:
template<parameter-list> function-declaration
函數(shù)模板在形式上分為兩部分:模板、函數(shù)。在函數(shù)前面加上template<...>就成為函數(shù)模板,因此對函數(shù)的各種修飾(inline、constexpr等)需要加在function-declaration上,而不是template前。如
template<typename T>
inline T min(const T &, const T &);
parameter-list是由英文逗號(,)分隔的列表,每項可以是下列之一:
序號 | 名稱 | 說明 |
---|---|---|
1 | 非類型形參 | 已知的數(shù)據類型,如整數(shù)、指針等,C++11中有三種形式: int N int N = 1: 帶默認值,該值必須是一個常量或常量表達式 int ...N: 模板參數(shù)包(可變參數(shù)模板) |
2 | 類型形參 | swap值用的形式,格式為: typename name[ = default] typename ... name: 模板參數(shù)包 |
3 | 模板模板形參 | 沒錯有兩個"模板",這個比較復雜,有興趣的同學可以參考 cppreference之模板形參與模板實參 |
上面swap函數(shù)模板,使用了類型形參。函數(shù)模板就像是一種契約,任何滿足該契約的類型都可以做為模板實參。而契約就是函數(shù)實現(xiàn)中,模板實參需要支持的各種操作。上面swap中T需要滿足的契約為:支持拷貝構造和賦值。
template<typename T>
void swap(T &a, T &b) {
T tmp{a}; // 契約一:T需要支持拷貝構造
a = b; // 契約二:T需要支持賦值操作
b = tmp;
}
三、函數(shù)模板不是函數(shù)
剛才我們提到函數(shù)模板用來定義一族函數(shù),而不是一個函數(shù)。C++是一種強類型的語言,在不知道T的具體類型前,無法確定swap需要占用的棧大小(參數(shù)棧,局部變量),同時也不知道函數(shù)體中T的各種操作如何實現(xiàn),無法生成具體的函數(shù)。只有當用具體類型去替換T時,才會生成具體函數(shù),該過程叫做函數(shù)模板的實例化。當在main函數(shù)中調用swap(a,b)
時,編譯器推斷出此時T
為int
,然后編譯器會生成int版的swap函數(shù)供調用。所以相較普通函數(shù),函數(shù)模板多了生成具體函數(shù)這一步。如果我們只是編寫了函數(shù)模板,但不在任何地方使用它(也不顯式實例化),則編譯器不會為該函數(shù)模板生成任何代碼。
函數(shù)模板實例化分為隱式實例化和顯式實例化。
3.1 隱式實例化
仍以swap為例,我們在main中調用swap(a,b)
時,就發(fā)生了隱式實例化。當函數(shù)模板被調用,且在之前沒有顯式實例化時,即發(fā)生函數(shù)模板的隱式實例化。如果模板實參能從調用的語境中推導,則不需要提供。
#include <iostream>
template<typename T>
void print(const T &r) {
std::cout << r << std::endl;
}
int main() {
// 隱式實例化print<int>(int)
print(1);
// 實例化print<char>(char)
print<>('c');
// 仍然是隱式實例化,我們希望編譯器生成print<double>(double)
print<double>(1);
}
3.2 顯式實例化
在函數(shù)模板定義后,我們可以通過顯式實例化的方式告訴編譯器生成指定實參的函數(shù)。顯式實例化聲明會阻止隱式實例化。
template<typename R, typename T1, typename T2>
R add(T1 a, T2 b) {
return static_cast<R>(a + b);
}
// 顯式實例化
template double add<double, int, double>(int, double);
// 顯式實例化, 推導出第三個模板實參
template int add<int, int>(int, int);
// 全部由編譯器推導
template double add(double, double);
如果我們在顯式實例化時,只指定部分模板實參,則指定順序必須自左至右依次指定,不能越過前參模板形參,直接指定后面的。
四、函數(shù)模板的使用
4.1 使用非類型形參
#include <iostream>
// N必須是編譯時的常量表達式
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ù)的返回值類型取決于函數(shù)參數(shù)某種運算后的類型。對于這種情況可以采用auto關鍵字作為返回值占位符。
template<typename T1, typename T2>
auto multi(T a, T b) -> decltype(a * b) {
return a * b;
}
decltype操作符用于查詢表達式的數(shù)據類型,也是C++11標準引入的新的運算符,其目的是解決泛型編程中有些類型由模板參數(shù)決定,而難以表示的問題。為何要將返回值后置呢?
// 這樣是編譯不過去的,因為decltype(a*b)中,a和b還未聲明,編譯器不知道a和b是什么。
template<typename T1, typename T2>
decltype(a*b) multi(T a, T b) {
return a*+ b;
}
//編譯時會產生如下錯誤: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ù)。這是因為C++編譯器在解析類的時候就要確定虛函數(shù)表(vtable)的大小,如果允許一個虛函數(shù)是函數(shù)模板,那么就需要在解析這個類之前掃描所有的代碼,找出這個模板成員函數(shù)的調用或顯式實例化操作,然后才能確定虛函數(shù)表的大小,而顯然這是不可行的。
4.4 函數(shù)模板重載
函數(shù)模板之間、普通函數(shù)和模板函數(shù)之間可以重載。編譯器會根據調用時提供的函數(shù)參數(shù),調用能夠處理這一類型的最佳匹配版本。在匹配度上,一般按照如下順序考慮:
順序 | 行為 |
---|---|
1 | 最符合函數(shù)名和參數(shù)類型的普通函數(shù) |
2 | 特殊模板(具有非類型形參的模板,即對T有類型限制) |
3 | 普通模板(對T沒有任何限制的) |
4 | 通過類型轉換進行參數(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;
// 可以通過空模板實參列表來限定編譯器只匹配函數(shù)模板
std::cout << max<>("en", "ch") << std::endl;
}
輸出
max(&, &) = 2
max(*, *) = 2
max(&, &, &) = 3
max(const char *, const char *) = en
max(*, *) = en
可以通過空模板實參列表來限定編譯器只匹配函數(shù)模板,比如main函數(shù)中的最后一條語句。
4.5 函數(shù)模板特化
當函數(shù)模板需要對某些類型進行特別處理,這稱為函數(shù)模板的特化。當我們定義一個特化版本時,函數(shù)參數(shù)類型必須與一個先前聲明的模板中對應的類型匹配。函數(shù)模板特化的本質是實例化一個模板,而非重載它。因此,特化不影響編譯器函數(shù)匹配。
template<typename T1, typename T2>
int compare(const T1 &a, const T2 b) {
return a - b;
}
// 對const char *進行特化
template<>
int compare(const char * const &a, const char * const &b) {
return strcmp(a, b);
}
上面的例子中針對const char *的特化,我們其實可以通過函數(shù)重載達到相同效果。因此對于函數(shù)模板特化,目前公認的觀點是沒什么用,并且最好別用。Why Not Specialize Function Templates?
但函數(shù)模板特化和重載在重載決議時有些細微的差別。這些差別中比較有用的一個是阻止某些隱式轉換。如當你只有void foo(int)時,以浮點類型調用會發(fā)生隱式轉換,這可以通過特化來阻止:
template <class T> void foo(T);
template <> void foo(int) {}
foo(3.0); // link error,阻止float隱式轉換為int
雖然模板配重載也可以達到同樣的效果,但特化版的意圖更加明確。
函數(shù)模板及其特化版本應該聲明在同一個頭文件中。所有同名模板的聲明應該放在前面,然后是這些模板的特化版本。
五、變參函數(shù)模板(模板參數(shù)包)
這是C++11引入的新特性,用來表示任意數(shù)量的模板形參。其語法樣式如下:
template<typename ...Args> // Args: 模板參數(shù)包
void foo(Args ... args); // args: 函數(shù)參數(shù)包
在模板形參Args的左邊出現(xiàn)三個英文點號"...",表示Args是零個或多個類型的列表,是一個模板參數(shù)包(template parameter pack)。正如其名稱一樣,編譯器會將Args所表示的類型列表打成一個包,將其當做一個特殊類型處理。相應的函數(shù)參數(shù)列表中也有一個函數(shù)參數(shù)包。與普通模板函數(shù)一樣,編譯器從函數(shù)的實參推斷模板參數(shù)類型,與此同時還會推斷包中參數(shù)的數(shù)量。
// sizeof...() 是C++11引入的參數(shù)包的操作函數(shù),用來取參數(shù)的數(shù)量
template<typename ...Args>
int length(Args ... args) {
return sizeof...(Args);
}
// 以下語句將在屏幕打印出:2
std::cout << length(1, "hello") << std::endl;
變參函數(shù)模板主要用來處理既不知道要處理的實參的數(shù)目也不知道它們的類型時的場景。既然我們對實參數(shù)量以及類型都一無所知,那么我們怎么使用它呢?最常用的方法是遞歸。
5.1 遞歸
通過遞歸來遍歷所有的實參,這需要一點點的技巧,需要給出終止遞歸的條件,否則遞歸將無限進行。
#include <iostream>
// 遞歸終止
void print() { /// 1
std::cout << std::endl;
}
// 打印綁定到t的實參
template<typename T, typename... Args>
void print(const T &t, const Args &... args) { /// 2
std::cout << t << (sizeof...(args) > 0 ? ", " : "");
// 編譯時展開:通過在args右邊添加省略號(...)進行展開,打印參數(shù)包中剩余的參數(shù)
print(args...);
}
int main() {
print(1, "hello", "C++", 11);
return 0;
}
//輸出: 1, hello, C++, 11
該例子的技巧在于,函數(shù)2提供了const T &t參數(shù),保證至少有一個參數(shù),避免了與函數(shù)1在args為0時的沖突。需要注意的是,遞歸是指編譯器遞歸,不是運行過程時的遞歸調用。實際上編譯器為函數(shù)2生成了4個重載版本,并依次調用。下圖是在運行時的調用棧,可以看到共有5個重載版本的print函數(shù),4個遞歸展開的函數(shù)2,外加函數(shù)1。遞歸最終結束在函數(shù)1處。
5.2 包擴展
對于一個參數(shù)包,不管是模板參數(shù)包還是函數(shù)參數(shù)包,我們對它能做的只有兩件事:sizeof...()和包擴展。前面我們說過編譯器將參數(shù)包當作一個類型來處理,因此使用的時候需要將其展開,展開時我們需要提供用于每個元素的處理模式(pattern)。包擴展就是對參數(shù)包中的每一個元素應用模式,獲取得擴展后的列表。最簡單的包擴展方式就是我們在上節(jié)中看到的const Args &...
和args...
,該擴展是將其擴展為構成元素。C++11還支持更復雜的擴展模式,如:
#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) {
// 復雜的包擴展方式
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;
}
}
運行程序將產生如下輸出:
vec.size => 3
"1"
"hello"
"world"
擴展過程中模式(pattern)會獨立地應用于包中的每一個元素。同時pattern也可以接受多個參數(shù),并非僅僅只能接受參數(shù)包。
5.3 參數(shù)包的轉發(fā)
C++11中,我們可以同時使用變參函數(shù)模板和std::forward機制來編寫函數(shù),將實參原封不動地傳遞給其它函數(shù)。其中典型的應用是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ù)模板重點在模板。表示這是一個模板,用來生成函數(shù)。
模板函數(shù)重點在函數(shù)。表示的是由一個模板生成而來的函數(shù)。
6.2 cv限定
cv限定是指函數(shù)參數(shù)中有const、volatile或mutable限定。已指定、推導出或從默認模板實參獲得所有模板實參時,函數(shù)參數(shù)列表中每次模板形參的使用都會被替換成對應的模板實參。替換后:
- 所有數(shù)組類型和函數(shù)類型參數(shù)被調整成為指針
- 所有頂層cv限定符從函數(shù)參數(shù)被丟棄,如在普通函數(shù)聲明中。
頂層cv限定符的去除不影響參數(shù)類型的使用,因為它出現(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);
// 兩個不同函數(shù)有同一類型,但在函數(shù)中, t有不同的cv限定
f<int>(1); // 函數(shù)類型是 void(int) , t 為 int
f<const int>(1); // 函數(shù)類型是 void(int) , t 為 const int
// 二個不同函數(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多線程-內存模型 |
目錄 | 下一篇 C++11多線程-類模板 |
---|