C++模板詳解(一):函數模板的概念和特性

函數模板是指這樣的一類函數:能夠用多種不一樣數據類型的參數進行調用,表明了一個函數家族。它的外表和普通的函數很類似,惟一的區別就是:函數中的有些元素是未肯定的,這些元素將在使用的時候才被實例化。先來看一個簡單的例子:ios

1、定義一個簡單的函數模板

下面的這個例子就定義了一個模板函數,它會返回兩個參數中最大的那一個:函數

// 文件:"max.hpp"
template<typename T>
inline const T& max(const T& x, const T& y)
{
    return x < y ? y : x;
}

這個函數模板定義了一個「返回兩個值中最大者」的函數家族,而參數的類型尚未肯定,用類型模板參數T來肯定。模板參數須要使用以下的方式來聲明:spa

template< 模板參數列表 >

在這個例子中,模板參數列表爲:typename T。關鍵字typename引入了T這個類型模板參數。固然了,可使用任何標識符做爲類型模板參數的名稱。咱們可使用任何類型(基本數據類型、類類型)來實例化該函數模板,只要所使用的數據類型提供了函數模板中所須要的操做便可。例如,在這個例子中,類型T須要支持operator <,由於a和b就是經過這個操做符來比較大小的。設計

鑑於歷史緣由,也可使用關鍵字class來取代typename來定義類型模板參數,然而應該儘量地使用typename指針

2、使用函數模板

下面的程序使用了上面定義的這個函數模板:code

#include <iostream>
#include <string>
#include "max.hpp"

using namespace std;

int main(int argc, char *argv[])
{
    cout << max(4, 3) << endl; // 使用int類型實例化了函數模板,並調用了該函數實例。
    cout << max(4.0, 3.0) << endl; // 使用double類型實例化了函數模板,並調用了該函數實例。
    cout << max(string("hello"), string("world")) << endl; // 使用string類型實例化了函數模板,
                                                           // 並調用了該函數實例。
    return 0;
}

一般而言,並非把模板編譯成一個能夠處理任何類型的單一實體,而是針對於實例化函數模板參數的每種類型,都從函數模板中產生出一個獨立的函數實體。所以,針對於每種類型,模板代碼都被編譯了一次。這種用具體類型代替模板參數的過程,叫作模板的實例化。它產生了一個新的函數實例(與面向對象程序設計中的實例化不一樣)。對象

若是試圖基於一個不支持模板內部所使用的操做的類型實例化一個模板,那麼將會引起一個編譯期錯誤:編譯器

std::complex<double> c1, c2;
max(c1, c2); // 編譯錯誤:std::complex並不支持運算符<

因此說:模板被編譯了兩次,分別發生於:string

  • 模板實例化以前,查看語法是否正確,此時可能會發現遺漏的分號等。
  • 模板實例化期間,檢查模板代碼, 查看是否全部的調用都有效。此時可能會發現無效的調用,例如實例化類型不支持某些函數調用等。

因此這引起了一個重要的問題:當使用函數模板而且引起模板實例化時,編譯器必須查看模板的定義。事實上,這就不一樣於普通的函數,由於對於普通的函數而言,只要有函數的聲明(甚至不須要定義),就能夠順利地經過編譯期。io

3、函數模板實參推斷

當咱們爲某些實參調用一個函數模板時,模板參數能夠由咱們所傳遞的實參來決定。

注意:函數模板在推斷參數類型時,不容許自動類型轉換,每一個類型模板參數都必須正確的匹配。

template<typename T>
inline const T& max(const T& x, const T& y)
{
    return x < y ? y : x;
}

int main()
{
    // 不能這樣調用:
    // max(10, 20.0); // 錯誤,由於函數模板中的類型推斷拒絕隱式類型轉換
    // 這是由於,沒法肯定到底應該使用哪一個參數類型來實例化這個模板函數。
    // 因此,C++拒絕了這種作法。 可用的解決方案:
    ::max(static_cast<double>(10), 20.0); // OK,由於兩個參數都爲double。
    ::max<double>(10, 20.0); // OK, 顯示指定參數,這樣能夠嘗試對參數進行類型轉換。
    return 0;
}

注意:模板實參推斷並不適合返回類型。由於返回類型並不會出如今函數調用參數的類型裏面。

因此,必需要顯示地指定返回類型:

template<typename T1, typename T2, typename RT>
inline RT func()
{
    // ...
    return RT();
}

int main(int argc, char *argv[])
{
    func<int>(); // 必須這樣顯示地指定返回類型才能夠,沒法進行自動類型推斷。
    return 0;
}

4、函數模板的重載

和普通的函數同樣,函數模板也能夠被重載。在下面的例子中,一個非模板函數能夠和一個同名的函數模板同時存在,這稱爲函數模板的特化。並且該函數模板還被實例化爲這個非模板函數。

// #1
inline const int& max(const int& a, const int& b)
{
    return a < b ? b : a;
}

// #2
template<typename T>
inline const T& max(const T& a, const T& b)
{
    return a < b ? b : a;
}

// #3
template<typename T>
inline const T& max(const T& a, const T& b, const T& c)
{
    return max(max(a, b), c);
}

int main(int argc, char *argv[])
{
    /*01*/max(7, 42, 68);       // 調用#3
    /*02*/max(7.0, 6.0);        // 調用#2
    /*03*/max('a', 'b');        // 調用#2
    /*04*/max(7, 42);           // 調用#1
    /*05*/max<>(7, 42);         // 調用#2
    /*06*/max<double>(7, 42);   // 調用#2可是沒有推斷參數
    /*07*/max('a', 42.7);       // 調用#1
    return 0;
}

總結以下:

  • 對於非模板函數和同名的函數模板,若是其它條件都是相同的話,那麼在調用的時候,重載解析過程當中會優先調用非模板函數,而不會實例化模板(04)。
  • 若是模板能夠產生一個具備更好匹配的函數,那麼將選擇模板(02, 03)。
  • 還能夠顯示地指定一個空的模板參數列表,告訴編譯器:必須使用模板來匹配(05)。
  • 因爲函數模板拒絕隱式類型轉換,因此當全部的模板都沒法匹配,可是發現能夠經過強制類型轉換來匹配一個非模板函數時,將調用那個函數(07)。

5、函數模板重載的注意事項

在重載函數模板時,請謹記:將對函數聲明的改變限制在如下兩種狀況中:

  • 改變參數的數目
  • 顯示指定模板的參數(即函數模板特化)

不然,極可能會致使非預期的結果,例如在下面的例子中,模板函數是使用引用進行傳參的,然而在其中的一個重載中(其實是針對char*進行的特化),卻使用了值傳遞的方式,這將會致使不可預期的結果:

template<typename T>
inline const T& max(const T& a, const T& b)
{
    return a < b ? b : a;
}

// #2: 存在隱患,由於其它的重載都是以引用傳遞參數,而這個重載版本
// 卻使用了值傳遞,不符合上面介紹的須要遵照的兩個可變條件。
inline const char* max(const char* a, const char* b)
{
    return std::strcmp(a, b) < 0 ? b : a;
}

template<typename T>
inline const T& max(const T& a, const T& b, const T& c)
{
    // 這裏對max(a, b)的調用,若是調用了函數#2,
    // 那麼將會返回一個局部的值,若是剛好這個局部的值
    // 又比c大,那麼將會返回一個指向局部變量的指針,
    // 這是很危險的(非預期的行爲)。
    return max( max(a, b), c );
}

int main()
{
    char str1[] = "frederic";
    char str2[] = "anica";
    char str3[] = "lucas";

    char* p1 = str1;
    char* p2 = str2;
    char* p3 = str3;

    // 這種用法是錯的,這是由於:
    // max(a, b)返回的是一個指針,這個指針是一個局部的對象,
    // 而且這個局部的對象頗有可能會被返回。
    auto result = max(p1, p2, p3);
    return 0;
}
相關文章
相關標籤/搜索