模板(template)是為了支持泛型編程(Generic programming)而存在的,所謂泛型,也就是不依賴于具體類型,wiki對其定義如下
Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.
為了更直觀的了解,我們先看看相對于一般的編程方式,范型編程是怎么樣的
#include <iostream>
int max_normal(int a, int b) {
return a > b? a : b;
}
template <typename T>
T max_generic(T a, T b) {
return a > b? a : b;
}
#define Log(x) std::cout<<x<<std::endl;
int main() {
//int
Log(max_normal(1, 2)); //2
Log(max_generic(1, 2));//2
//float
Log(max_normal(1.1, 1.2));//1
Log(max_generic(1.1, 1.2));//1.2
return 0;
}
對于多種數據類型,普通函數要聲明不同的版本
float max(float, float);
int max(int, int);
這將導致大量的重復工作,為此,我們需要解放自己,這時候泛型編程登場了
template<typename T>
T max(T x, T y) {
return x > y? x : y;
}
就一個函數的事,多么簡潔而優(yōu)雅!模板就如藍圖一樣,對于實例化模板參數的每一種類型,都從模板中產生一個不同的實體,下面就是測試
#include <iostream>
template <typename T>
T max_generic(T a, T b) {
return a > b? a : b;
}
#define Log(x) std::cout<<x<<std::endl;
int main() {
//使用 int 進行實例化
Log(max_generic(1, 2));
//使用 double 進行實例化
Log(max_generic(1.1, 1.2));
return 0;
}
然后我們就會得到兩個實例...
//自動生成的
int max_generic(int a, int b) {
return a > b? a : b;
}
double max_generic(double a, double b) {
return a > b? a : b;
}
如果還不相信的話,我們就來看看對應的匯編代碼
g++ -g -c source.cpp & objdump -S source.o > source_obj.s
1.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_main:
; int main() {
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: bf 01 00 00 00 movl $1, %edi
d: be 02 00 00 00 movl $2, %esi
; max(1, 2);
12: e8 00 00 00 00 callq 0 <_main+0x17>
17: f2 0f 10 05 91 00 00 00 movsd 145(%rip), %xmm0
1f: f2 0f 10 0d 91 00 00 00 movsd 145(%rip), %xmm1
; max(1.1, 1.2);
27: 89 45 fc movl %eax, -4(%rbp)
2a: e8 00 00 00 00 callq 0 <_main+0x2F>
2f: 31 c0 xorl %eax, %eax
; }
31: f2 0f 11 45 f0 movsd %xmm0, -16(%rbp)
36: 48 83 c4 10 addq $16, %rsp
3a: 5d popq %rbp
3b: c3 retq
3c: 0f 1f 40 00 nopl (%rax)
__Z3maxIiET_S0_S0_:
; T max(T x, T y) {
40: 55 pushq %rbp
41: 48 89 e5 movq %rsp, %rbp
44: 89 7d fc movl %edi, -4(%rbp)
47: 89 75 f8 movl %esi, -8(%rbp)
; return x > y? x : y;
4a: 8b 75 fc movl -4(%rbp), %esi
4d: 3b 75 f8 cmpl -8(%rbp), %esi
50: 0f 8e 0b 00 00 00 jle 11 <__Z3maxIiET_S0_S0_+0x21>
56: 8b 45 fc movl -4(%rbp), %eax
59: 89 45 f4 movl %eax, -12(%rbp)
5c: e9 06 00 00 00 jmp 6 <__Z3maxIiET_S0_S0_+0x27>
61: 8b 45 f8 movl -8(%rbp), %eax
64: 89 45 f4 movl %eax, -12(%rbp)
67: 8b 45 f4 movl -12(%rbp), %eax
6a: 5d popq %rbp
6b: c3 retq
6c: 0f 1f 40 00 nopl (%rax)
__Z3maxIdET_S0_S0_:
; T max(T x, T y) {
70: 55 pushq %rbp
71: 48 89 e5 movq %rsp, %rbp
74: f2 0f 11 45 f8 movsd %xmm0, -8(%rbp)
79: f2 0f 11 4d f0 movsd %xmm1, -16(%rbp)
; return x > y? x : y;
7e: f2 0f 10 45 f8 movsd -8(%rbp), %xmm0
83: 66 0f 2e 45 f0 ucomisd -16(%rbp), %xmm0
88: 0f 86 0f 00 00 00 jbe 15 <__Z3maxIdET_S0_S0_+0x2D>
8e: f2 0f 10 45 f8 movsd -8(%rbp), %xmm0
93: f2 0f 11 45 e8 movsd %xmm0, -24(%rbp)
98: e9 0a 00 00 00 jmp 10 <__Z3maxIdET_S0_S0_+0x37>
9d: f2 0f 10 45 f0 movsd -16(%rbp), %xmm0
a2: f2 0f 11 45 e8 movsd %xmm0, -24(%rbp)
a7: f2 0f 10 45 e8 movsd -24(%rbp), %xmm0
ac: 5d popq %rbp
ad: c3 retq
我們可以看到max一共有兩個函數名
- __Z3maxIiET_S0_S0 =
int max(int, int) - __Z3maxIdET_S0_S0,=
double max(double, double)
好嘞,對底層的驗證點到為止,那么接下來我們將迎來重頭戲
模板參數的推導(deduction)
函數模版有兩種類型的參數
- template<typename T> : T是模板參數
- max(T x, T y) : x, y是調用參數
模板參數要求必須要全部推導出來,函數模板可以通過傳入實參來推導模板參數,如果推導失敗,那么就會發(fā)生錯誤
max(1, 1.5); //max(int, double), 推導失敗
如果沒有全部推導,也會發(fā)生錯誤
template<typename T1, typename T2>
void foo(T1 x);
foo(1); //只有T1被推導出來,T2沒有被推導,錯誤!
foo<int, int>(1); //顯示指定模板參數,成功
一般而言,不會去轉化實參類型以匹配已有的實例,不過有些情況除外
-
const轉換:
- 如果模板形參為const引用,則其可以接受const或非const引用
- 如果模板形參為const指針,則其可以接受const或非const指針
- 如果模板形參不是引用或指針(值傳遞),則形參和實參都忽略const
-
數組或函數到指針的轉換
- 如果模板形參不是引用或指針(值傳遞),則數組會轉化為指針,數組實參將當作指向其第一個元素的指針;
- 如果模板形參不是引用或指針(值傳遞),則函數會轉化為指針,函數實參將當作指向函數類型的指針;
template<typename T>
void fobj(T x) {
x = 3;
}
template<typename T>
void fref(const T& v) {
}
int main() {
int a = 1;
const int b = 2;
fobj(b);
fref(a);
fref(b);
return 0;
}
但是有時候,模板推斷可能會出乎我們的預料
template<typename T>
void fref(T& x) {
}
int main() {
const int b = 4;
fref(b);//ok!
return 0;
}
道理上來說,這樣的推斷是錯誤的,但是我們可以看到在函數中并沒有對x進行任何修改,因此可以匹配
但是如果在函數中加入x=3那么,編譯就會報錯,這就在情理之中了
另外從C++11開始,已經可以用模板來推導模板參數了
例如
#include <iostream>
template<typename T, int F>
void fun(T i) {
i = i + F;
}
template<typename T>
void test(T i) {
std::cout << std::is_same<T, void(*)(int)>::value << std::endl; // true, T被推導為函數指針void(*)(int)
i(3);
}
int main() {
test(fun<int, 2>);
return 0;
}
函數模板的重載與匹配
#include <iostream>
#include <typeinfo>
#define Log(x) std::cout<<x<<std::endl
const int & max(const int &a, const int &b) {
Log("normal");
return a > b? a : b;
}
template<typename T>
T const & max(const T &a, const T &b) {
Log("template");
return a > b? a : b;
}
template<typename T>
T const & max(const T &a, const T &b, const T &c) {
Log("template");
return max(max(a, b), c);
}
int main() {
Log(max(1, 2));
Log(max(1.2, 2.4));
Log(max(1, 2, 3));
return 0;
}
注意,實例化模版也是有代價的,如果能通同時匹配到一般函數和模板函數,就使用一般函數
模板不允許隱式轉換,因此如果兩個類型不同,就會考慮一般函數(如上),max(int, float)就會匹配一般函數