六、模板

Link

模板(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)用后能返回truefalse的東西。例如:

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è)用到它的編譯單元。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • C++ 標(biāo)準(zhǔn)模板庫的核心包括以下三個(gè)組件: 容器(Containers)deque、list、vector、map...
    RobertY閱讀 470評(píng)論 0 1
  • 一、模板編程 1.模板編程的必要性 在c++中,變量的聲明必須指出它的類型,提高了編譯運(yùn)行效率,但是在某些場(chǎng)合下就...
    送分童子笑嘻嘻閱讀 480評(píng)論 0 0
  • 推斷的過程 每個(gè)實(shí)參-參數(shù)對(duì)的推斷都是獨(dú)立的,如果結(jié)果矛盾推斷就會(huì)失敗 即使所有推斷不發(fā)生矛盾,也可能推斷失敗 對(duì)...
    奇點(diǎn)創(chuàng)客閱讀 535評(píng)論 0 1
  • 本文聊聊C++中的模板類型推導(dǎo)和auto。兩者其實(shí)是一樣的,前者推導(dǎo)T的類型,后者推導(dǎo)auto的類型。本文初創(chuàng)于公...
    金戈大王閱讀 3,650評(píng)論 3 1
  • 表達(dá)式模板是為了支持一種數(shù)值數(shù)組的類引入的技術(shù)。比如希望像內(nèi)置類型一樣對(duì)數(shù)組進(jìn)行下列操作,要在支持這種緊湊寫法的同...
    奇點(diǎn)創(chuàng)客閱讀 275評(píng)論 0 1

友情鏈接更多精彩內(nèi)容