函式模板是指這樣的一類函式:可以用多種不同資料型別的引數進行呼叫,代表了一個函式家族,它的外表和普通的函式很相似,唯一的區別就是:函式中的有些元素是未確定的,這些元素將在使用的時候才被實體化,先來看一個簡單的例子:
一、定義一個簡單的函式模板
下面的這個例子就定義了一個模板函式,它會回傳兩個引數中最大的那一個:
// 檔案:"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;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/64981.html
標籤:C++
上一篇:C++ operator new & placement new
下一篇:C++對于C故有問題的改進
