C++11泛型-函數模板

許了IP屬地: 湖南
0.289字數 2,825

一、為什么要有函數模板

在泛型編程出現前,我們要實現一個swap函數得這樣寫:

void swap(int &a, int &b) {
    int tmp{a};
    a = b;
    b = tmp;
}

但這個函數只支持int型的變量交換,如果我們要做float, long, double, std::string等等類型的交換時,只能不斷加入新的重載函數。這樣做不但代碼冗余,容易出錯,還不易維護。C++函數模板有效解決了這個問題。函數模板擺脫了類型的限制,提供了通用的處理過程,極大提升了代碼的重用性。

二、什么是函數模板

cppreference中給出的定義是"函數模板定義一族函數",怎么理解呢?我們先來看一段簡單的代碼

#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);  // 使用函數模板
    std::cout << "a=" << a << ", b=" << b << std::endl;
}

swap支持多種類型的通用交換邏輯。它跟普通C++函數的區別在于其函數聲明(declaration)前面加了個template<typename T>,這句話告訴編譯器,swap中(函數參數、返回值、函數體中)出現類型T時,不要報錯,T是一個通用類型。
函數模板的格式:

template<parameter-list> function-declaration

函數模板在形式上分為兩部分:模板、函數。在函數前面加上template<...>就成為函數模板,因此對函數的各種修飾(inline、constexpr等)需要加在function-declaration上,而不是template前。如

template<typename T>
inline T min(const T &, const T &);

parameter-list是由英文逗號(,)分隔的列表,每項可以是下列之一:

序號 名稱 說明
1 非類型形參 已知的數據類型,如整數、指針等,C++11中有三種形式:
int N
int N = 1: 帶默認值,該值必須是一個常量或常量表達式
int ...N: 模板參數包(可變參數模板)
2 類型形參 swap值用的形式,格式為:
typename name[ = default]
typename ... name: 模板參數包
3 模板模板形參 沒錯有兩個"模板",這個比較復雜,有興趣的同學可以參考
cppreference之模板形參與模板實參

上面swap函數模板,使用了類型形參。函數模板就像是一種契約,任何滿足該契約的類型都可以做為模板實參。而契約就是函數實現中,模板實參需要支持的各種操作。上面swap中T需要滿足的契約為:支持拷貝構造和賦值。

template<typename T>
void swap(T &a, T &b) {
    T tmp{a};  // 契約一:T需要支持拷貝構造
    a = b;     // 契約二:T需要支持賦值操作
    b = tmp;
}

三、函數模板不是函數

剛才我們提到函數模板用來定義一族函數,而不是一個函數。C++是一種強類型的語言,在不知道T的具體類型前,無法確定swap需要占用的棧大小(參數棧,局部變量),同時也不知道函數體中T的各種操作如何實現,無法生成具體的函數。只有當用具體類型去替換T時,才會生成具體函數,該過程叫做函數模板的實例化。當在main函數中調用swap(a,b)時,編譯器推斷出此時Tint,然后編譯器會生成int版的swap函數供調用。所以相較普通函數,函數模板多了生成具體函數這一步。如果我們只是編寫了函數模板,但不在任何地方使用它(也不顯式實例化),則編譯器不會為該函數模板生成任何代碼。

函數模板實例化

函數模板實例化分為隱式實例化和顯式實例化。

3.1 隱式實例化

仍以swap為例,我們在main中調用swap(a,b)時,就發生了隱式實例化。當函數模板被調用,且在之前沒有顯式實例化時,即發生函數模板的隱式實例化。如果模板實參能從調用的語境中推導,則不需要提供。

#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 顯式實例化

函數模板定義后,我們可以通過顯式實例化的方式告訴編譯器生成指定實參的函數。顯式實例化聲明會阻止隱式實例化。

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);

如果我們在顯式實例化時,只指定部分模板實參,則指定順序必須自左至右依次指定,不能越過前參模板形參,直接指定后面的。

函數模板顯式實例化

四、函數模板的使用

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

有些時候我們會碰到這樣一種情況,函數的返回值類型取決于函數參數某種運算后的類型。對于這種情況可以采用auto關鍵字作為返回值占位符。

template<typename T1, typename T2>
auto multi(T a, T b) -> decltype(a * b) {
    return a * b;
}

decltype操作符用于查詢表達式的數據類型,也是C++11標準引入的新的運算符,其目的是解決泛型編程中有些類型由模板參數決定,而難以表示的問題。為何要將返回值后置呢?

// 這樣是編譯不過去的,因為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 類成員函數模板

函數模板可以做為類的成員函數。

#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

需要注意的是:函數模板不能用作虛函數。這是因為C++編譯器在解析類的時候就要確定虛函數表(vtable)的大小,如果允許一個虛函數是函數模板,那么就需要在解析這個類之前掃描所有的代碼,找出這個模板成員函數的調用或顯式實例化操作,然后才能確定虛函數表的大小,而顯然這是不可行的。

4.4 函數模板重載

函數模板之間、普通函數和模板函數之間可以重載。編譯器會根據調用時提供的函數參數,調用能夠處理這一類型的最佳匹配版本。在匹配度上,一般按照如下順序考慮:

順序 行為
1 最符合函數名和參數類型的普通函數
2 特殊模板(具有非類型形參的模板,即對T有類型限制)
3 普通模板(對T沒有任何限制的)
4 通過類型轉換進行參數匹配的重載函數
#include <iostream>

template<typename T>
const T &max(const T &a, const T &b) {
    std::cout << "max(&, &) = ";
    return a > b ? a : b;
}

// 函數模板重載
template<typename T>
const T *max(T *a, T *b) {
    std::cout << "max(*, *) = ";
    return *a > *b ? a : b;
}

// 函數模板重載
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;
}

// 普通函數
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;
    // 可以通過空模板實參列表來限定編譯器只匹配函數模板
    std::cout << max<>("en", "ch") << std::endl;
}

輸出

max(&, &) = 2
max(*, *) = 2
max(&, &, &) = 3
max(const char *, const char *) = en
max(*, *) = en

可以通過空模板實參列表來限定編譯器只匹配函數模板,比如main函數中的最后一條語句。

4.5 函數模板特化

當函數模板需要對某些類型進行特別處理,這稱為函數模板的特化。當我們定義一個特化版本時,函數參數類型必須與一個先前聲明的模板中對應的類型匹配。函數模板特化的本質是實例化一個模板,而非重載它。因此,特化不影響編譯器函數匹配。

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 *的特化,我們其實可以通過函數重載達到相同效果。因此對于函數模板特化,目前公認的觀點是沒什么用,并且最好別用Why Not Specialize Function Templates?

但函數模板特化和重載在重載決議時有些細微的差別。這些差別中比較有用的一個是阻止某些隱式轉換。如當你只有void foo(int)時,以浮點類型調用會發生隱式轉換,這可以通過特化來阻止:

template <class T> void foo(T);
template <> void foo(int) {}
foo(3.0); // link error,阻止float隱式轉換為int

雖然模板配重載也可以達到同樣的效果,但特化版的意圖更加明確。

函數模板及其特化版本應該聲明在同一個頭文件中。所有同名模板的聲明應該放在前面,然后是這些模板的特化版本。

五、變參函數模板(模板參數包)

這是C++11引入的新特性,用來表示任意數量的模板形參。其語法樣式如下:

template<typename ...Args>  // Args: 模板參數包
void foo(Args ...  args);   // args: 函數參數包

在模板形參Args的左邊出現三個英文點號"...",表示Args是零個或多個類型的列表,是一個模板參數包(template parameter pack)。正如其名稱一樣,編譯器會將Args所表示的類型列表打成一個包,將其當做一個特殊類型處理。相應的函數參數列表中也有一個函數參數包。與普通模板函數一樣,編譯器從函數的實參推斷模板參數類型,與此同時還會推斷包中參數的數量。

// sizeof...() 是C++11引入的參數包的操作函數,用來取參數的數量
template<typename ...Args>
int length(Args ...  args) {
    return sizeof...(Args);
}

// 以下語句將在屏幕打印出:2
std::cout << length(1, "hello") << std::endl;

變參函數模板主要用來處理既不知道要處理的實參的數目也不知道它們的類型時的場景。既然我們對實參數量以及類型都一無所知,那么我們怎么使用它呢?最常用的方法是遞歸

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右邊添加省略號(...)進行展開,打印參數包中剩余的參數
    print(args...);
}

int main() {
    print(1, "hello", "C++", 11);
    return 0;
}
//輸出: 1, hello, C++, 11

該例子的技巧在于,函數2提供了const T &t參數,保證至少有一個參數,避免了與函數1在args為0時的沖突。需要注意的是,遞歸是指編譯器遞歸,不是運行過程時的遞歸調用。實際上編譯器為函數2生成了4個重載版本,并依次調用。下圖是在運行時的調用棧,可以看到共有5個重載版本的print函數,4個遞歸展開的函數2,外加函數1。遞歸最終結束在函數1處。

compiler_recursive.png

5.2 包擴展

對于一個參數包,不管是模板參數包還是函數參數包,我們對它能做的只有兩件事:sizeof...()包擴展。前面我們說過編譯器將參數包當作一個類型來處理,因此使用的時候需要將其展開,展開時我們需要提供用于每個元素的處理模式(pattern)。包擴展就是對參數包中的每一個元素應用模式,獲取得擴展后的列表。最簡單的包擴展方式就是我們在上節中看到的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也可以接受多個參數,并非僅僅只能接受參數包。

5.3 參數包的轉發

C++11中,我們可以同時使用變參函數模板和std::forward機制來編寫函數,將實參原封不動地傳遞給其它函數。其中典型的應用是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 函數模板 .vs. 模板函數

函數模板重點在模板。表示這是一個模板,用來生成函數。
模板函數重點在函數。表示的是由一個模板生成而來的函數。

6.2 cv限定

cv限定是指函數參數中有const、volatile或mutable限定。已指定、推導出或從默認模板實參獲得所有模板實參時,函數參數列表中每次模板形參的使用都會被替換成對應的模板實參。替換后:

  • 所有數組類型和函數類型參數被調整成為指針
  • 所有頂層cv限定符從函數參數被丟棄,如在普通函數聲明中。

頂層cv限定符的去除不影響參數類型的使用,因為它出現于函數中:

template <typename T> void f(T t);
template <typename X> void g(const X x);
template <typename Z> void h(Z z, Z *zp);

// 兩個不同函數有同一類型,但在函數中, t有不同的cv限定
f<int>(1);       // 函數類型是 void(int) , t 為 int
f<const int>(1); // 函數類型是 void(int) , t 為 const int

// 二個不同函數擁有同一類型和同一 x
// (指向此二函數的指針不相等,且函數局域的靜態變量可以擁有不同地址)
g<int>(1);       // 函數類型是 void(int) , x 為 const int
g<const int>(1); // 函數類型是 void(int) , x 為 const int

// 僅丟棄頂層 cv 限定符:
h<const int>(1, NULL); // 函數類型是 void(int, const int*) 
                       // z 為 const int , zp 為 int*
上一篇
C++11多線程-內存模型
目錄 下一篇
C++11多線程-類模板
最后編輯于
©著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
5人點贊
總資產7共写了1.5W字获得123个赞共176个粉丝
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容