1.1 定義函數模板
template<typename T>
T max(T a,T b) {
return b < a ? a : b;
}
1.2 使用函數模板
std::cout << max(7,42) << std::endl;
std::cout << max(1.1,2.2) << std::endl;
std::cout << max("math","mathematics") << std::endl;
模板不是被編譯成可以處理任何類型的單個函數。相反,編譯器會針對每一個使用該模板的類型生成對應的函數。例如,max(7,42)
的調用在語義上相當於調用了:
int max(int a,int b) {
return b < a ? a : b;
}
double、string同理。
將模板參數替換成具體參數類型的過程叫做instantiation
,這個過程會產生一個instance of template
。
1.3 兩階段翻譯 Two-Phase Translation
如果某一特定參數類型不支持模板內的操作,那么編譯階段會報錯,例如:
std::complex<float> c1,c2; //不支持 max中的 < 操作,編譯階段會報錯
...
max(c1,c2);
模板會分成兩個階段進行”編譯“:
- 在不進行模板
instantiation
的definition time
階段,此時會忽略模板參數,檢查如下方面:- 語法錯誤,包括缺失分號。
- 使用未定義參數。
- 如果static assertion不依賴模板參數,會檢查是否通過static assertion.
- 在
instantiation
階段,會再次檢查模板里所有代碼的正確性,尤其是那些依賴模板參數的部分。
例如:
template<typename T>
void foo(T t) {
undeclared(); // first-phase compile-time error if undeclared() unknown
undeclared(t); // second-phase compile-time error if undeclared(T) unknown
static_assert(sizeof(int) > 10,"int too small"); // first-phase compile-time error
static_assert(sizeof(T) > 10, "T too small"); // second-phase compile-time error
}
1.3.1 模板的編譯和鏈接問題
大多數人會按照如下方式組織非模板代碼:
- 將類或者其他類型聲明放在頭文件(.hpp、.H、.h、.hh、.hxx)中。
- 將函數定義等放到一個單獨的編譯單元文件中(.cpp、.C、.c、.cc、.cxx)。
但是這種組織方式在包含模板的代碼中卻行不通,例如:
頭文件:
// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP
定義函數模板的文件:
// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x) {
std::cout << typeid(x).name() << '\n';
}
在另一個文件中使用該模板:
// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main() {
double ice = 3.0;
printTypeof(ice); // call function template for type double
}
在c/c++中,當編譯階段發現一個符號(printTypeof)沒有定義只有聲明時,編譯器會假設它的定義在其他文件中,所以編譯器會留一個”坑“給鏈接器linker,讓它去填充真正的符號地址。
但是上面說過,模板是比較特殊的,需要在編譯階段進行instantiation
,即需要進行模板參數類型推斷,實例化模板,當然也就需要知道函數的定義。但是由於上面兩個cpp文件都是單獨的編譯單元文件,所以當編譯器編譯myfirstmain.cpp
時,它沒有找到模板的定義,自然也就沒有instantiation
。
解決辦法就是我們把模板的聲明和定義都放在一個頭文件。大家可以看一下自己環境下的vector等STL源文件,就是把類的聲明和定義都放在了一個文件中。
1.4 多模板參數
template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
return b < a ? a : b;
}
...
auto m = max(4, 7.2); // 注意:返回類型是第一個模板參數T1 的類型
但是問題正如注釋中說的,max的返回值類型總是T1。如果我們調用max(42, 66.66)
,返回值則是66。
一般有三個方法解決這個問題:
- 引入額外模板參數作為返回值類型
- 讓編譯器自己找出返回值類型
- 將返回值聲明為兩個模板參數的公共類型,比如int和float,公共類型就是float
1.4.1 引入額外模板參數作為返回值類型
在函數模板的參數類型推導過程中,一般我們不用顯式指定模板參數類型。但是當模板參數不能根據傳遞的參數推導出來時,我們就需要顯式的指定模板參數類型。
template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b);
RT是不能根據函數的參數列表推導出來的,所以我們需要顯式的指定:
max<int, double, double>(4, 7.2);
或者我們改變模板參數列表順序,這種情況只需顯式的指定一個參數類型即可:
template<typename RT typename T1, typename T2> //RT變為第一個模板參數
RT max(T1 a, T2 b);
...
max<double>(4, 7.2);
1.4.2 讓編譯器自己找出返回值類型
在C++11中,我們可以利用auto和trailing return type來讓編譯器找出返回值類型:
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b) {
return b < a ? a : b;
}
decltype后面的文章會講到,這里只需知道它可以獲取到表達式的類型。
我們可以寫的更簡單點:
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b) { // true ? a : b
return b < a ? a : b;
}
關於?:返回值規則可以參考這個:Conditional Operator: ? :
看到true ? a : b
不要奇怪為什么是true,這里的重點不是計算返回值,而是得到返回值類型。
在C++14中,我們可以省略trailing return type:
template<typename T1, typename T2>
auto max (T1 a, T2 b) {
return b < a ? a : b;
}
1.4.3 將返回值聲明為兩個模板參數的公共類型
c++11新特性std::common_type
可以產生幾個不同類型的共同類型,其實核心意思跟上面說的差不多:
template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max(T1 a, T2 b) {
return b < a ? a : b;
}
在c++14中,可以更簡單的寫:
template <typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b) {
return b < a ? a : b;
}
這里使用_t
后綴讓我們不用寫typename
和::type
。類似的還有_v,這個在c++14的type traits
里很常見。
1.5 默認模板參數
這個很像函數的默認參數,直接看例子:
template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b) {
return b < a ? a : b;
}
auto a = max(4, 7.2);
auto b = max<double,int,long double>(7.2, 4);
正如第二個用法,如果我們想顯示的指明RT的類型,必須顯示的指出全部三個參數類型。但是與函數默認參數不同的是,我們可以將默認參數放到第一個位置:
template <typename RT = long, typename T1, typename T2>
RT max(T1 a, T2 b) {
return b < a ? a : b;
}
int i;
long l;
…
max(i, l); // 返回值類型是long (RT 的默認值)
max<int>(4, 42); //返回int,因為其被顯式指定
1.6 重載函數模板
這個跟普通函數重載也類似:
// maximum of two int values:
int max(int a, int b) {
return b < a ? a : b;
}
// maximum of two values of any type:
template <typename T>
T max(T a, T b) {
return b < a ? a : b;
}
int main() {
max(7, 42); // calls the nontemplate for two ints
max(7.0, 42.0); // calls max<double> (by argument deduction)
max('a', 'b'); // calls max<char> (by argument deduction)
max<>(7, 42); // calls max<int> (by argument deduction)
max<double>(7, 42); // calls max<double> (no argument deduction)
max('a', 42.7); // calls the nontemplate for two ints
}
這里需要解釋下最后一個max('a', 42.7)
。因為在模板參數類型推導過程中不允許類型自動轉換,但是調用普通函數是允許的,所以這個會調用非模板函數。
ps. 由於函數模板重載,所以函數模板並不像類模板一樣可以進行偏特化。
還有兩點關於重載的基本原則需要了解一下:
1.6.1 重載時最好不要隨便改變模板參數個數,最好可以顯示的指定模板參數類型
下面是段有問題的代碼:
// maximum of two values of any type (call-by-reference)
template <typename T> T const &max(T const &a, T const &b) {
return b < a ? a : b;
}
// maximum of two C-strings (call-by-value)
char const *max(char const *a, char const *b) {
return std::strcmp(b, a) < 0 ? a : b;
}
// maximum of three values of any type (call-by-reference)
template <typename T> T const &max(T const &a, T const &b, T const &c) {
return max(max(a, b), c); // error if max(a,b) uses call-by-value
}
int main() {
auto m1 = max(7, 42, 68); // OK
char const *s1 = "frederic";
char const *s2 = "anica";
char const *s3 = "lucas";
auto m2 = max(s1, s2, s3); // run-time ERROR
}
問題出現在return max (max(a,b), c);
,因為char const *max(char const *a, char const *b)
的參數是按值傳遞,max(a,b)
會產生一個指向已經銷毀的棧幀地址,這會導致未定義行為。
1.6.2 確保所有被重載的函數模板在使用時已經被聲明定義
// maximum of two values of any type:
template <typename T>
T max(T a, T b) {
std::cout << "max<T>()\n";
return b < a ? a : b;
}
// maximum of three values of any type:
template <typename T>
T max(T a, T b, T c) {
return max(max(a, b), c);
}
// maximum of two int values:
int max(int a, int b) {
std::cout << "max(int,int) \n";
return b < a ? a : b;
}
int main() {
max(47, 11, 33); // max<T>()
}
這點很好理解。
(完)
朋友們可以關注下我的公眾號,獲得最及時的更新: