函數模板是指這樣的一類函數:可以用多種不同數據類型的參數進行調用,代表了一個函數家族。它的外表和普通的函數很相似,唯一的區別就是:函數中的有些元素是未確定的,這些元素將在使用的時候才被實例化。先來看一個簡單的例子:
一、定義一個簡單的函數模板
下面的這個例子就定義了一個模板函數,它會返回兩個參數中最大的那一個:
// 文件:"max.hpp"
template<typename T>
inline const T& max(const T& x, const T& y)
{
return x < y ? y : x;
}
這個函數模板定義了一個“返回兩個值中最大者”的函數家族,而參數的類型還沒有確定,用類型模板參數T來確定。模板參數需要使用如下的方式來聲明:
template< 模板參數列表 >
在這個例子中,模板參數列表為:typename T。關鍵字typename引入了T這個類型模板參數。當然了,可以使用任何標識符作為類型模板參數的名稱。我們可以使用任何類型(基本數據類型、類類型)來實例化該函數模板,只要所使用的數據類型提供了函數模板中所需要的操作即可。例如,在這個例子中,類型T需要支持operator <,因為a和b就是通過這個操作符來比較大小的。
鑒於歷史原因,也可以使用關鍵字class來取代typename來定義類型模板參數,然而應該盡可能地使用typename。
二、使用函數模板
下面的程序使用了上面定義的這個函數模板:
#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並不支持運算符<
所以說:模板被編譯了兩次,分別發生於:
- 模板實例化之前,查看語法是否正確,此時可能會發現遺漏的分號等。
- 模板實例化期間,檢查模板代碼, 查看是否所有的調用都有效。此時可能會發現無效的調用,例如實例化類型不支持某些函數調用等。
所以這引發了一個重要的問題:當使用函數模板並且引發模板實例化時,編譯器必須查看模板的定義。事實上,這就不同於普通的函數,因為對於普通的函數而言,只要有函數的聲明(甚至不需要定義),就可以順利地通過編譯期。
三、函數模板實參推斷
當我們為某些實參調用一個函數模板時,模板參數可以由我們所傳遞的實參來決定。
注意:函數模板在推斷參數類型時,不允許自動類型轉換,每個類型模板參數都必須正確的匹配。
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;
}
四、函數模板的重載
和普通的函數一樣,函數模板也可以被重載。在下面的例子中,一個非模板函數可以和一個同名的函數模板同時存在,這稱為函數模板的特化。而且該函數模板還被實例化為這個非模板函數。
// #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)。
五、函數模板重載的注意事項
在重載函數模板時,請謹記:將對函數聲明的改變限制在以下兩種情況中:
- 改變參數的數目
- 顯示指定模板的參數(即函數模板特化)
否則,很可能會導致非預期的結果,例如在下面的例子中,模板函數是使用引用進行傳參的,然而在其中的一個重載中(實際上是針對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;
}
