模板(template)是一種類或者函數(shù),我們用一組類型或值去參數(shù)化它。 我們用模板表示這樣一種概念:它是某種通用的東西,我么可以通過指定參數(shù)來生成類型或函數(shù),至于這種參數(shù),比方說是vector的元素類型double。
?
我們那個(gè)承載double的vector,可以泛化成一個(gè)承載任意類型的vector,只要把它變成一個(gè)template,并用一個(gè)類型參數(shù)替代具體的double類型。例如:
template<typename T>
class Vector {
private:
T* elem; // elem指向一個(gè)數(shù)組,該數(shù)組承載sz個(gè)T類型的元素
int sz;
public:
explicit Vector(int s); // 構(gòu)造函數(shù):建立不變式,申請(qǐng)資源
~Vector() { delete[] elem; } // 析構(gòu)函數(shù):釋放資源
// ... 復(fù)制和移動(dòng)操作 ...
T& operator[](int i); // 為非const Vector取下標(biāo)元素
const T& operator[](int i) const; // 為const Vector取下標(biāo)元素
int size() const { return sz; }
};
成員函數(shù)可能有相似的定義:
template<typename T> Vector<T>::Vector(int s) {
if (s<0)
throw Negative_size{};
elem = new T[s];
sz = s;
}
template<typename T>
const T& Vector<T>::operator[](int i) const {
if (i<0 || size()<=i)
throw out_of_range{"Vector::operator[]"};
return elem[i];
}
想讓我們的Vecor支持區(qū)間-for循環(huán),就必須定義適當(dāng)?shù)腷egin()和end()函數(shù):
template<typename T>
T* begin(Vector<T>& x) {
return x.size() ? &x[0] : nullptr; // 指向第一個(gè)元素的指針或者nullptr
}
template<typename T>
T* end(Vector<T>& x) {
return x.size() ? &x[0]+x.size() : nullptr; // 指向末尾元素身后位置
}
模板是個(gè)編譯期機(jī)制,因此使用它們跟手寫的代碼相比,并不會(huì)在運(yùn)行時(shí)帶來額外的負(fù)擔(dān)。 實(shí)際上,Vector<double>生成的代碼與第4章Vector版本的代碼一致。 更進(jìn)一步,標(biāo)準(zhǔn)庫vector<double>生成的代碼很可能更好 (因?yàn)閷?shí)現(xiàn)它的時(shí)候下了更多功夫)。
模板附帶一組模板參數(shù),叫做 實(shí)例化(instantiation) 或者 特化(specialization)。編譯過程靠后的部分,在 實(shí)例化期(instantiation time),程序里用到的每個(gè)實(shí)例都會(huì)被生成。生成的代碼會(huì)經(jīng)歷類型檢查,以便它們與手寫代碼具有同樣的類型安全性。 遺憾的是,此種類型檢查通常處于編譯過程較晚的階段——在實(shí)例化期。
?
除了類型參數(shù),模板還可以接受值參數(shù)。例如:
template<typename T, int N>
struct Buffer {
using value_type = T;
constexpr int size() { return N; }
T[N];
// ...
};
別名(value_type)和 constexpr 函數(shù)允許我們(只讀)訪問模板參數(shù)。
值參數(shù)在很多語境里都很有用。例如:Buffer允許我們創(chuàng)建任意容量的緩沖區(qū),卻不使用自由存儲(chǔ)區(qū)(動(dòng)態(tài)內(nèi)存):
Buffer<char,1024> glob; // 用于字符的全局緩沖區(qū)(靜態(tài)分配)
void fct() {
Buffer<int,10> buf; // 用于整數(shù)的局部緩沖區(qū)(在棧上)
// ...
}
值模板參數(shù)必須是常量表達(dá)式。
?
考慮一下標(biāo)準(zhǔn)庫模板pair的應(yīng)用:
pair<int,double> p = {1,5.2};
auto p = make_pair(1,5.2); // p 是個(gè) pair<int,double>
pair p = {1,5.2}; // p 是個(gè) pair<int,double>
template<typename T>
class Vector {
public:
Vector(int);
Vector(initializer_list<T>); // 初始化列表構(gòu)造函數(shù)
// ...
};
Vector v1 {1,2,3}; // 從初始值類型推導(dǎo)v1的元素類型
Vector v2 = v1; // 從v1的元素類型推導(dǎo)v2的元素類型
auto p = new Vector{1,2,3}; // p 指向一個(gè) Vector<int>
Vector<int> v3(1); // 此處,我們需要顯式指定元素類型(未提及元素類型)
Vector<string> vs1 {"Hello", "World"}; // Vector<string>
Vector vs {"Hello", "World"}; // 推導(dǎo)為 Vector<const char*> (詫異嗎?)
Vector vs2 {"Hello"s, "World"s}; // 推導(dǎo)為 Vector<string>
Vector vs3 {"Hello"s, "World"}; // 報(bào)錯(cuò):初始化列表類型不單一
如果無法從構(gòu)造函數(shù)參數(shù)推導(dǎo)某個(gè)模板參數(shù),我們可以用 推導(dǎo)引導(dǎo) 輔助。考慮:
template<typename T>
class Vector2 {
public:
using value_type = T;
// ...
Vector2(initializer_list<T>); // 初始化列表構(gòu)造函數(shù)
template<typename Iter>
Vector2(Iter b, Iter e); // [b:e) 區(qū)間構(gòu)造函數(shù)
// ...
};
Vector2 v1 {1,2,3,4,5}; // 元素類型是 int
Vector2 v2(v1.begin(),v1.begin()+2);// 元素類型是 int,而非 Iterator
很明顯,v2應(yīng)該是個(gè)Vector2<int>,但是因?yàn)槿鄙佥o助信息,編譯器無法推導(dǎo)出來。 這段代碼僅表明:有個(gè)構(gòu)造函數(shù)接收一對(duì)同類型的值。 缺乏概束的語言支持,對(duì)于該類型,編譯器無法假設(shè)任何情況。 如果想進(jìn)行推導(dǎo),可以在Vector2的聲明后添加一個(gè)推導(dǎo)指引:
template<typename Iter>
Vector2(Iter,Iter) -> Vector2<typename Iter::value_type>;
推導(dǎo)指引的效果通常很微妙,因此在設(shè)計(jì)類模板的時(shí)候,盡量別依靠它。 不過,標(biāo)準(zhǔn)庫里滿是(目前還)未使用concept且?guī)в羞@種二義性的類, 因此它們用了不少的推導(dǎo)指引。
?
template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v) {
for (auto x : s)
v+=x;
return v;
}
模板參數(shù)Value和函數(shù)參數(shù)v,允許調(diào)用者指定這個(gè)累加函數(shù)的類型和初值(累加到和里的變量):
void user(Vector<int>& vi, list<double>& ld, vector<complex<double>>& vc) {
int x = sum(vi,0); // 承載 int 的vector的和(與 int 相加)
double d = sum(vi,0.0); // 承載 int 的vector的和(與 double 相加)
double dd = sum(ld,0.0); // 承載 double 的vector的和
auto z = sum(vc,complex{0.0,0.0}); // 承載 complex<double>s 的vector的和
}
把int加到double上的意義在于能優(yōu)雅地處理超出int上限地?cái)?shù)值。注意sum<Sequence,Value>從函數(shù)參數(shù)中推導(dǎo)模板參數(shù)的方法。巧的是不需要顯式指定它們。
函數(shù)模板可用于成員函數(shù),但不能是virtual成員。在一個(gè)程序里,編譯器無法知曉某個(gè)模板的全部實(shí)例,因此無法生成虛函數(shù)表vtbl。
?
有一種特別有用的模板是函數(shù)對(duì)象(function object)(也叫仿函數(shù)(functor)),用于定義可調(diào)用對(duì)象。例如:
template<typename T>
class Less_than {
const T val; // 參與比對(duì)的值
public:
Less_than(const T& v) :val{v} { }
bool operator()(const T& x) const { return x<val; } // 調(diào)用運(yùn)算符
};
名為operator()的函數(shù)實(shí)現(xiàn)“函數(shù)調(diào)用”、“調(diào)用”或“應(yīng)用”運(yùn)算符()。
可以為某些參數(shù)類型定義Less_than類型的具名變量:
Less_than lti {42}; // lti(i) 將把i用<號(hào)與42作比(i<42)
Less_than lts {"Backus"s}; // lts(s) 將把s用<號(hào)與"Backus"作比(s<"Backus")
Less_than<string> lts2 {"Naur"}; // "Naur"是個(gè)C風(fēng)格字符串,因此需要用 <string> 獲取正確的 <
可以像調(diào)用函數(shù)一樣調(diào)用這樣的對(duì)象:
void fct(int n, const string& s) {
bool b1 = lti(n); // true if n<42
bool b2 = lts(s); // true if s<"Backus"
// ...
}
這種函數(shù)對(duì)象廣泛用做算法的參數(shù)。例如,可以統(tǒng)計(jì)使特定謂詞為true的值的數(shù)量:
template<typename C, typename P>
// requires Sequence<C> && Callable<P,Value_type<P>>
int count(const C& c, P pred) {
int cnt = 0;
for (const auto& x : c)
if (pred(x))
++cnt;
return cnt;
}
謂詞(predicate)是調(diào)用后能返回true或false的東西。例如:
void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) {
cout << "number of values less than " << x << ": " << count(vec,Less_than{x}) << '\n';
cout << "number of values less than " << s << ": " << count(lst,Less_than{s}) << '\n';
}
這些函數(shù)對(duì)象的妙處在于,它們隨身攜帶參與比較的值。 我們無需為每個(gè)值(以及每種類型)寫一個(gè)單獨(dú)的函數(shù),也無需引入一個(gè)惱人的全局變量去持有這個(gè)值。還有,類似于Less_than這種函數(shù)對(duì)象易于內(nèi)聯(lián),因此調(diào)用Less_than遠(yuǎn)比間接的函數(shù)調(diào)用高效。攜帶數(shù)據(jù)的能力再加上高效性,使函數(shù)對(duì)象作為算法參數(shù)特別有用。
用在通用算法中的函數(shù)對(duì)象,可指明其關(guān)鍵運(yùn)算的意義(例如Less_than之于count()),通常被稱為策略對(duì)象(policy object)。
?
還有個(gè)隱式生成函數(shù)對(duì)象的寫法:
void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) {
cout << "number of values less than " << x
<< ": " << count(vec,[&](int a){ return a<x; })
<< '\n';
cout << "number of values less than " << s
<< ": " << count(lst,[&](const string& a){ return a<s; })
<< '\n';
}
[&](int a){return a<x;}這個(gè)寫法叫l(wèi)ambda表達(dá)式。 它跟Less_than<int>{x}一樣會(huì)生成函數(shù)對(duì)象。此處的[&]是一個(gè)抓取列表(capture list),表明lambda函數(shù)體內(nèi)用到的所有局部名稱,將以引用的形式訪問。如果我們僅想“抓取”x,應(yīng)該這么寫:[&x]。如果我們把x的副本傳給生成的對(duì)象,就應(yīng)該這么寫:[=x]。不抓取任何東西寫[ ],以引用方式抓取所有局部名稱寫[&],以傳值方式抓取所有局部名稱寫:[=]。
使用lambda表達(dá)式方便、簡(jiǎn)略,但也略晦澀些。 對(duì)于繁復(fù)的操作(比方說超出一個(gè)表達(dá)式的內(nèi)容),我傾向于為它命名,以便明確用途,并讓它可以在程序中多處訪問。
再來看一個(gè)示例:
template<typename C, typename Oper>
void for_all(C& c, Oper op) {// 假定C是個(gè)承載指針的容器
// 要求 Sequence<C> && Callable<Oper,Value_type<C>>
for (auto& x : c)
op(x); // 把每個(gè)元素指向的對(duì)象傳引用給 op()
}
void user2() {
vector<unique_ptr<Shape>> v;
while (cin)
v.push_back(read_shape(cin));
for_all(v,[](unique_ptr<Shape>& ps){ ps->draw(); }); // draw_all()
for_all(v,[](unique_ptr<Shape>& ps){ ps->rotate(45); }); // rotate_all(45)
}
我把unique_ptr<Shape>&傳給lambda表達(dá)式,這樣for_all()就無需關(guān)心對(duì)象存儲(chǔ)的方式了。確切的說,這些for_all()函數(shù)不影響傳入的Shape生命期,lambda表達(dá)式的函數(shù)體使用參數(shù)時(shí),就像用舊式的指針一樣。
跟函數(shù)一樣,lambda表達(dá)式也可以泛型。例如:
template<class S>
void rotate_and_draw(vector<S>& v, int r) {
for_all(v, [](auto& s){ s->rotate(r); s->draw(); });
}
此處的auto,像變量聲明里那樣,意思是初始值(在調(diào)用中,實(shí)參初始化形參)接受任何類型。這讓帶有auto的lambda表達(dá)式成了模板,一個(gè)泛型lambda。
可以用任意容器調(diào)用這個(gè)泛型的rotate_and_draw(), 只要該容器內(nèi)的對(duì)象能執(zhí)行draw()和rotate()。例如:
void user4() {
vector<unique_ptr<Shape>> v1;
vector<Shape*> v2;
// ...
rotate_and_draw(v1, 45);
rotate_and_draw(v2, 90);
}
?
要定義出好的模板,我們需要一些輔助的語言構(gòu)造:
- 依賴于類型的值:
變量模板(variable template) - 針對(duì)類型和模板的別名:
別名模板(alias template) - 編譯期選擇機(jī)制:
if constexpr - 針對(duì)類型和表達(dá)式屬性的編譯期查詢機(jī)制:
requires表達(dá)式
另外,constexpr函數(shù)和static_assert也經(jīng)常參與模板設(shè)計(jì)和應(yīng)用。
對(duì)于構(gòu)建通用、基本的抽象,這些基礎(chǔ)機(jī)制是主要工具。
在使用某個(gè)類型時(shí),經(jīng)常會(huì)需要該類型的常量和值。這理所當(dāng)然也發(fā)生在我們使用類模板的的時(shí)候: 當(dāng)我們定義了C<T>,通常會(huì)需要類型T以及依賴T的其它類型的常量和變量。以下示例出自一個(gè)流體力學(xué)模擬[Garcia,2015]:
template <class T>
constexpr T viscosity = 0.4;
template <class T>
constexpr space_vector<T> external_acceleration = { T{}, T{-9.8}, T{} };// space_vector是個(gè)三維向量
auto vis2 = 2*viscosity<double>;
auto acc = external_acceleration<float>;
顯然,可以用適當(dāng)類型的任意表達(dá)式作為初始值。考慮:
template<typename T, typename T2>
constexpr bool Assignable =
is_assignable<T&,T2>::value; // is_assignable 是個(gè)類型 trait
template<typename T>
void testing() {
static_assert(Assignable<T&,double>, "can't assign a double");
static_assert(Assignable<T&,string>, "can't assign a string");
}
經(jīng)歷一些大刀闊斧的變動(dòng),這個(gè)點(diǎn)子成了概束(第7章)定義的關(guān)鍵。
出人意料的是,為類型或者模板引入一個(gè)同義詞很有用。 例如,標(biāo)準(zhǔn)庫頭文件<cstddef>包含一個(gè)size_t的別名,可能是這樣:
using size_t = unsigned int;
用于命名size_t的實(shí)際類型是實(shí)現(xiàn)相關(guān)的,因此在另一個(gè)實(shí)現(xiàn)里size_t可能是unsigned long。有了別名size_t的存在,就讓程序員能夠?qū)懗隹梢浦驳拇a。
對(duì)參數(shù)化類型來說,為模板參數(shù)相關(guān)的類型提供別名是很常見的。例如:
template<typename T>
class Vector {
public:
using value_type = T;
// ...
};
實(shí)際上,每個(gè)標(biāo)準(zhǔn)庫容器都提供了value_type作為其值類型的名稱(第11章)。 對(duì)于所有遵循此慣例的容器,我們都能寫出可行的代碼。例如:
template<typename C>
using Value_type = typename C::value_type; // C 的元素的類型
template<typename Container>
void algo(Container& c) {
Vector<Value_type<Container>> vec; // 結(jié)果保存在這里
// ...
}
通過綁定部分或全部模板參數(shù),可以用別名機(jī)制定義一個(gè)新模板。例如:
template<typename Key, typename Value>
class Map {
// ...
};
template<typename Value>
using String_map = Map<string,Value>;
String_map<int> m; // m 是個(gè) Map<string,int>
思考編寫這樣一個(gè)操作,它在slow_and_safe(T)和simple_and_fast(T)里二選一。這種問題充斥在基礎(chǔ)代碼中——那些通用性和性能優(yōu)化都重要的場(chǎng)合。傳統(tǒng)的解決方案是寫一對(duì)重載的函數(shù),并基于trait(第13章)選出最適宜的那個(gè),比方說標(biāo)準(zhǔn)庫里的is_pod。如果涉及類體系,slow_and_safe(T)可提供通用操作, 而某個(gè)繼承類可以用simple_and_fast(T)的實(shí)現(xiàn)去重載它。
在 C++17 里,可以利用一個(gè)編譯期if:
template<typename T> void update(T& target) {
// ...
if constexpr(is_pod<T>::value)
simple_and_fast(target); // 針對(duì)“簡(jiǎn)單舊式的數(shù)據(jù)”
else
slow_and_safe(target);
// ...
}
is_pod<T>是個(gè)類型trait,它辨別某個(gè)類型可否低成本復(fù)制。
僅被選定的if constexpr分支被實(shí)例化。此方案即提供了性能優(yōu)化,又實(shí)現(xiàn)了優(yōu)化的局部性。
重要的是,if constexpr并非文本處理機(jī)制,不會(huì)破壞語法、類型和作用域的常見規(guī)則。例如:
template<typename T>
void bad(T arg) {
if constexpr(Something<T>::value)
try {// 語法錯(cuò)誤
g(arg);
if constexpr(Something<T>::value)
} catch(...) { /* ... */ }// 語法錯(cuò)誤
}
如果允許類似的文本操作,會(huì)嚴(yán)重破壞代碼的可靠性,而且對(duì)依賴于新型程序表示技術(shù) (比方說“抽象語法樹(abstract syntax tree)”)的工具,會(huì)造成問題。
忠告
[1] 可應(yīng)用于很多參數(shù)類型的算法,請(qǐng)用模板去表達(dá);
[2] 請(qǐng)用模板去表達(dá)容器;
[3] 請(qǐng)用模板提升代碼的抽象層級(jí);
[4] 模板是類型安全的,但它的類型檢查略有些遲滯;
[5] 讓構(gòu)造函數(shù)或者函數(shù)模板去推導(dǎo)模板參數(shù)類型;
[6] 使用算法的時(shí)候,請(qǐng)用函數(shù)對(duì)象作參數(shù);
[7] 如果需要簡(jiǎn)單的一次性函數(shù)對(duì)象,采用lambda表達(dá)式;
[8] 虛成員函數(shù)無法作為模板成員函數(shù);
[9] 用模板別名簡(jiǎn)化符號(hào)表示,并隱藏實(shí)現(xiàn)細(xì)節(jié);
[10] 使用模板時(shí),確保其定義(不僅僅是聲明)在作用域內(nèi);
[11] 模板提供編譯期的“鴨子類型(duck typing)”;
[12] 模板不支持分離編譯:把模板定義#include進(jìn)每個(gè)用到它的編譯單元。