泛型编程

从普通模板开始

使用普通模板注意以下几点。

废弃的模板导出

对于模板成员函数,C++11后基本废弃了“模板导出”特性,即应该尽量将模板成员函数的声明和定义放在头文件(内联实现),而并非单独在源文件给出模板的实现,因为编译器的复杂性因此大大增加甚至不再支持;

自动推导

模板类型可以放在参数、返回值或者函数体内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

template<typename T1,typename T2, typename T3>
T3 add(T1 a,T2 b){
return a+b;
}

int main(){
int a = 2;
float b = 3.14;
cout<<add<int,float,float>(a,b);
return 0;
}

上述例子缺省任意显式类型都会被报错,因为传统的template仅支持有限的类型推导,如下例子:此处a+b的类型并不重要,因为最后结果都会被强制转换成T3(假如不满足加法类型或者类型转换编译器负责报错),所以调用时仅需要指定一个类型,这个不可推导的类型必须放在第一个模板参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

template<typename T3,typename T1,typename T2>
T3 add(T1 a,T2 b){
return static_cast<T3>(a+b);
}

int main(){
int a = 2;
float b = 3.14;
cout<<add<int>(a,b);
return 0;
}

C++11以后因为引入了decltype类型推导和匿名函数,支持这样的推导:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

template<typename T1,typename T2, typename T3 = decltype(T1()+T2())> //T3如果显式指定,不再使用自动推导
T3 add(T1 a,T2 b){
return a+b;
}

int main(){
int a = 2;
float b = 3.14;
cout<<add<int,float>(a,b);
return 0;
}

非类型参数

模板函数的意义是定义泛型的函数行为,因此常见的template参数一般是typename或class,这类参数称为类型参数;template也支持非类型参数,这些参数必须是整数类型,包括int/bool/普通指针/函数指针等,绝不能是浮点数类型,这种情况下就像在模板中定义了一种宏一样,例如(这个例子可能具有局限性,memset用于单字节填充,如果T是int等应该使用循环填值,此处仅作方便):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string.h>
using namespace std;

template<typename T, int Size>
T printf(){
T array[Size];
memset((void*)array, 'x', Size); //单字节填充避免垃圾内存
return array[Size-1];
}

int main(){
cout<<printf<char,5>(); //注意不要遗漏括号,否则就是输出函数指针了
return 0;
}

对于指针复杂类型参数,template更显玄学:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string.h>
using namespace std;

template<typename T1,typename T2, int(*foo)(T1,T2)>
auto printf(T1 a,T2 b) -> decltype(foo(a,b)){
return foo(a,b);
}

template<typename T1,typename T2>
int add(T1 a,T2 b){
return static_cast<int>(a+b);
}

int main(){
cout<<printf<int,int,add<int,int>>(1,2)<<endl; //3
return 0;
}
再变态点,你甚至可以传入一个成员函数指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

template<typename T1,typename T2, typename T,int(T::*foo)(T1,T2)> //修改成成员函数指针
auto printf(T&obj,T1 a,T2 b) -> decltype((obj.*foo)(a,b)){
return (obj.*foo)(a,b);
}

class Person{
public:
int sub(int a,double b){
return a-b;
}
};

int main(){
Person p;
cout<<printf<int,double,Person,&Person::sub>(p,1,2.34)<<endl; //3
return 0;
}
玩脱了,我在工程中并没有见过这种写法,请注意不要忘记:浮点数不能作为template参数,因为计算机无法精确地存储浮点数,也无法做到精准的模板匹配,另一个方面,template参数必须在编译期确定,例如:
1
2
3
4
5
6
7
8
9
10
11
template<typename T, int Size>
T printf(){
T array[Size];
memset((void*)array, 'x', Size); //单字节填充避免垃圾内存
return array[Size-1];
}

void test(){
int x = 5;
cout<<printf<char,x>();
}
x的赋值发生在运行期,因此你会得到一个报错,除非你会constexpr特性将其声明为constexpr int x = 5