模板

概述

模板是一种通用的描述机制,使用模板允许使用通用类型来定义函数和类,在使用时,通用类型可被具体的类型来代替,模板的引入了一种全新的编程思维方式,称为“泛型编程”或“通用编程”

定义模板的必要性

像c/c++/java是编译型语言,先编译后运行,他们又强大的类型系统,也叫强类型语言,而python/javascript/go,在使用的过程中,一个变量可以表达多种类型,称为弱类型语言

强类型语言所有参与运算的对象在编译的时候就确定下来,并且编译程序将进行严格的类型检查,为了解决强类型的严格性和灵活性的冲突,也就是在严格的语法要求下尽可能提高灵活性,有以下方式:

1.函数重载

2.宏定义

3.模板

例如要实现各种类型的加法

1
2
3
4
template <class T>
T add(T x, T y){
return x + y;
}

模板的定义

模板作为代码重用机制的一种工具,它可以实现类型参数化,也就是把类型定义为参数,从未实现真正的代码可重用性

1
形式:template <tyname/class T1,class T2>

函数模板会在具体调用时根据函数模板实例化出模板函数,不需要显示定义这样的函数,模板发生的时机发生在编译时

函数模板

由函数模板到模板函数的过程称之为实例化,生成过程为:

函数模板-》生成相应的模板函数-》编译-》链接-》可执行文件

在进行模板实例化的时候,没有指明任何类型,函数模板在生成模板函数时通过传入的参数类型确定出模板类型,这种做法称为隐式实例化

我们在使用函数模板时还可以在函数名之后直接写上模板的类型参数列表,指定类型,这种用法称为显式实例化例如add<int>

函数模板的重载

分为两种:

函数模板与函数模板的重载

函数模板与普通函数的重载

函数模板与函数模板的重载

1
2
3
4
template <class T>
T add(T x, T y){
return x + y;
}

上面这个例子如果传入两个不同类型的参数,隐式实例化就会出问题,虽然可以使用显示实例化add<int>(a,b)即使a是short类型b是int类型也没关系,会把short类型强转为int类型,但是这是由风险的,因为int类型是可以存放short类型的数据的,但是如果有一个比int类型大的数据呢,例如a是double,那显式定义就会出现精度损失,对于这种情况就可采用函数模板与函数模板的重载

1
2
3
4
5
6
7
8
9
10
11
12
template <class T>
T add(T t1, T t2){
return t1 + t2;
}
template <class T1,class T2>
T1 add(T1 t1, T2 t2){
return t1 + t2;
}
void test(){
cout << add<double>(4,5.3) << endl;
//这里会调用模板一
}

但是这样仍然有一个问题,如果传入的参数是add(4,5.3),这样返回计算结果的时候仍然会发生一次类型转化,又发生了精度损失

而下面的传入的明明是两个类型不同的参数,怎么调用的是模板一呢?

如果这样调用就一定会调用模板二add<int,double>(4,5.3),因为只有模板二才支持指定两个模板参数,采用这种方式add<int>(4,5.3)调用的而也是模板二,什么情况?是因为指定的T1,没有指定T2,第一个参数都是int,不需要类型转换,传入的第二个参数推导出double,返回类型与第一个参数一致(就是没有指定任何的类型转化,第二个参数类型还不一致,就会走模板二),但上面这种情况add<double>(4,5.3)调用的是模板一,因为它指定第一个模板参数为double,而实际传入的参数是int,需要进行类型转换,第二个参数也是一个double,返回类型也是double,这下三个类型全是double了,直接用模板一就可以了,因为即使没有模板二,仍然可以正常调用

总结:在一个模块中定义多个通用模板的写法应该尽量避免使用,如果实在需要使用,也尽量使用隐式实例化的方式调用(通常函数模板会选择类型转换更少的模板)

函数模板与函数模板重载的条件:

1.名称必须相同

2.模板参数列表中的模板参数在函数中所处的位置不同(但是不建议这样写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T1,class T2>
T1 add(T1 t1, T2 t2){
return t1 + t2;
}
template <class T1,class T2>
T1 add(T2 t1, T1 t2){
return t1 + t2;
}

void test(){
int a = 4;
double b = 5.3;
add(a,b);//这怎么调用? 该调用模板一还是模板二?
//可以显式实例化
add<int>(a,b);//模板一对参数不需要进行任何的类型转换,那肯定选择这个
add<double>(a,b);//又调用成模板二了,因为模板二不需要进行任何的类型转换,那肯定选择这个
}

3.模板参数的个数不一样,可以构成重载

1
2
3
4
5
6
7
8
template <class T>
T add(T t1, T t2){
return t1 + t2;
}
template <class T1,class T2>
T1 add(T1 t1, T2 t2){
return t1 + t2;
}

函数模板与普通函数重载

普通函数优先于函数模板执行—因为普通函数更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//函数模板与普通函数重载
template <class T1, class T2>
T1 add(T1 t1, T2 t2)
{
return t1 + t2;
}

short add(short s1, short s2){
cout << "add(short,short)" << endl;
return s1 + s2;
}

void test1(){
short s1 = 1, s2 = 2;
cout << add(s1,s2) << endl; //调用普通函数

}

上述的例子是隐式实例化的例子,那显示实例化呢?

1
cout << add<short>(s1,s2) << endl;//使用函数模板

肯定使用函数模板,因为普通函数没有显式实例化那样的调用方法

并且可以发现,T1和T2推导出相同的类型是可以的,模板的多个类型参数可以实例化为相同的类型

头文件和实现文件的形式

为什么c++的头文件没有像c语言那样的.h后缀呢?

在一个源文件里面,函数模板的声明与定义分离是可以的,即使把函数模板的实现放在调用之下也是ok的,与普通函数一致。

1
2
3
4
5
6
7
8
9
//函数模板的声明
template <class T>
T add(T t1, T t2);

//函数模板的实现
template <class T>
T add(T t1,T t2){
return t1 + t2;
}

也可以把实现写在调用之后也是可以的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//函数模板的声明
template <class T>
T add(T t1, T t2);

void test(){
add(1,2);
}

//函数模板的实现
template <class T>
T add(T t1,T t2){
return t1 + t2;
}

与普通函数类似,那么,普通函数有一个很常规的写法,就是把声明写在一个.h文件里,然后有另一个文件来实现这些函数,那模板函数可以吗?

是不可以的,例如

1
2
3
4
//.h文件
//函数模板的声明
template <class T>
T add(T t1, T t2);
1
2
3
4
5
6
7
//.cc文件
#include "add.h"
//函数模板的实现
template <class T>
T add(T t1,T t2){
return t1 + t2;
}
1
2
3
4
5
6
//test.cc文件
#include "add.h"

void test(){
add(1,2);
}

编译的时候发现报错了

单独编译“实现文件”,使之生成目标文件,查看目标文件,会发现没有生成与add名称相关的函数。

单独编译测试文件,发现有与add名称相关的函数,但是没有地址,这就表示只有声明。

看起来和普通函数的情况有些不一样。

从原理上进行分析,函数模板定义好之后并不会直接产生一个具体的模板函数,只有在调用时才会实例化出具体的模板函数。


说白了就是模板函数在编译的时候没法生成出一个有效的声明,那么在实现文件中直接调用一下,让其生成一个有效的声明

1
2
3
4
5
6
7
8
9
10
11
//.cc文件
#include "add.h"
//函数模板的实现
template <class T>
T add(T t1,T t2){
return t1 + t2;
}

void test(){
add(1,2);
}

很奇怪,必须要在实现文件中使用一下该函数才能正常使用,类似的问题曾经出现在过inline函数中过,也就是在测试文件中调用的时候去头文件中去找对应的声明,发现是一个无效的声明,那可以效仿inline函数都把实现放在头文件中吗?

如果模板很多,都在头文件中实现,那么头文件就太冗余了

解决办法:把实现文件引入到.h文件中 #include "xxx.cc",既然这样拆分.h和.cc文件的意义是什么?

所以c+的.h和.cc都是头文件了,然后把.h文件的后缀去掉了,把.cc改名为.tcc

总结:

对模板的使用,必须要拿到模板的全部实现,如果只有一部分,那么推导也只能推导出一部分,无法满足需求。

换句话说,就是模板的使用过程中,其实没有了头文件和实现文件的区别,在头文件中也需要获取模板的完整代码,不能只有一部分。

模板的特化

在函数模板的使用中,有时候会有一些通用模板处理不了的情况,我们可以定义普通函数或特化模板来解决。虽然普通函数的优先级更高,但有些场景下是必须使用特化模板的

1
2
3
4
5
6
7
8
9
10
11
12
template <class T>
T add(T t1,T t2){
return t1 + t2;
}

void test(){
const char * p1 = "hello";
const char * p2 = "word";

add(p1,p2);//error 两个const char* 对象没法进行相加的操作
}

解决办法:

1.使用显示调用 add(p1,p2)

2.普通函数的优先级比函数模板高,可以顶一个普通函数来处理这种情况,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const char * add(const char *ptr1,const char * ptr2){
char *p = new char(strlen[ptr1]+strlen[ptr2]+1)();
strcpy(p,ptr1);
strcpy(p,ptr2);
return p;
}

void test(){
const char * p1 = "hello";
const char * p2 = "word";
const char * p =add(p1,p2);
cout << p << endl;
delete [] p;
}

3.如果不允许使用普通函数,改用特化模板,形式如下:

  1. template后直接跟 <> ,里面不写类型
  2. 在函数名后跟 <> ,其中写出要特化的类型template后直接跟 <> ,里面不写类型在函数名后跟 <> ,其中写出要特化的类型
1
2
3
4
5
6
7
8
template <>
const char * add<const char *>(const char *ptr1,const char * ptr2){
char *p = new char(strlen[ptr1]+strlen[ptr2]+1)();
strcpy(p,ptr1);
strcpy(p,ptr2);
return p;
}

针对通用模板无法处理的一些特殊的参数类型去定义特别的处理方式

注意:

使用模板特化时,必须要先有基础的函数模板

如果没有模板的通用形式,无法定义模板的特化形式。因为特化模板就是为了解决通用模板无法处理的特殊类型的操作。

特化版本的函数名、参数列表要和原基础的模板函数相同,避免不必要的错误。

使用模板的一些规则

  1. 在一个模块中定义多个通用模板的写法应该谨慎使用;
  2. 调用函数模板时尽量使用隐式调用,让编译器推导出类型;
  3. 无法使用隐式调用的场景只指定必须要指定的类型;
  4. 需要使用特化模板的场景就根据特化模板将类型指定清楚。

模板的参数类型

  1. 类型参数

    之前的T/T1/T2等等称为模板参数,也称为类型参数,类型参数T可以写成任何类型

  2. 非类型参数

    需要是整型数据, char/short/int/long/size_t等

    不能是浮点型,float/double不可以

定义模板时,在模板参数列表中除了类型参数还可以加入非类型参数。

此时,调用模板时需要传入非类型参数的值

例如

1
2
3
4
5
6
7
8
9
10
//T对应的就是类型参数,kbase对应的就是非类型参数
template <class T, int kBase>
T multiply(T t1,T t2){
return t1 * t2 *kBase;
}

cout test(){
cout << multiply(3,4) << endl;//error 没有指定kBase的值
cout << multiply<int,10>(3,4) << endl;//指定模板参数时需要按照模板参数列表的顺序来指定
}

其实模板参数也是可以赋默认值的

1
2
3
4
5
6
7
8
//T对应的就是类型参数,kbase对应的就是非类型参数
template <class T, int kBase = 10>
T multiply(T t1,T t2){
return t1 * t2 *kBase;
}
cout test(){
cout << multiply(3,4) << endl;//有默认值就可以用隐式实例化了,当然显示实例化也是可以的
}

其实class T = int也是允许的,也就是左边这个也可以赋默认值

1
2
3
4
5
6
7
8
9
template <class T = int, int kBase = 10>
T multiply(T t1,T t2){
return t1 * t2 *kBase;
}
cout test(){
cout << multiply<int,100>(1.2,1.2) << endl;//100
cout << multiply<int>(1.2,1.2) << endl;//10 类型参数指定为int
cout << multiply(1.2,1.2) << endl;//14.4 没有使用类型参数的默认值
}

可以得出优先级:指定的类型 > 推导出的类型 > 类型的默认参数

那类型的默认参数的使用场景有:

1
2
3
4
5
6
7
8
9
10
11
12
template <class T1,class T2 = double,int kBase = 10>
T1 multiply(T2 t1, T2 t2){
return t1 * t2 * kBase;
}

void test(){
double a =1.2,b = 1.2;

cout << multiply(a,b) << endl;//error 返回类型不确定

cout << multiply<int>(a,b) << endl;//14 int制定了返回值类型
}

为了让隐式实例化可以使用,可以给T1一个默认值

1
2
3
4
template <class T1 = int,class T2 = double,int kBase = 10>
T1 multiply(T2 t1, T2 t2){
return t1 * t2 * kBase;
}

成员函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Point
{
public:
Point(double x,double y)
: _x(x)
, _y(y)
{}

//定义一个成员函数模板
//将_x转换成目标类型
template <class T>
T convert()
{
return (T)_x;
}
private:
double _x;
double _y;
};


void test0(){
Point pt(1.1,2.2);
cout << pt.convert<int>() << endl;
cout << pt.convert() << endl; //error 要想让他可以调用可以给一个默认值
}

再增加一个模板成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Point
{
public:
Point(double x,double y)
: _x(x)
, _y(y)
{}

//定义一个成员函数模板
//将_x转换成目标类型
template <class T>
T convert()
{
return (T)_x;
}

template <class T>
T add(T t1){
return _x + _y +t1;
}

private:
double _x;
double _y;
};
void test(){
Point pt(1,2);
cout << pt.convert(3) << endl; //6 可以推导,所以隐式实例化可以直接调用
}

在add函数模板中可以访问Point的数据成员,说明成员函数模板的使用原理同普通函数模板一样,在调用时会实例化出一个模板成员函数。普通的成员函数会有隐含的this指针作为参数,这里生成的模板成员函数中也会有。如果定义一个static的成员函数模板,那么在其中就不能访问非静态数据成员。

但是要注意:成员函数模板不能加上virtual修饰,否则编译器报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Point
{
public:
Point(double x,double y)
: _x(x)
, _y(y)
{}

//定义一个成员函数模板
//将_x转换成目标类型
template <class T>
T convert()
{
return (T)_x;
}
template <calsss T>
static T add(T t1){
return _x + _y + t1;//error 没有this指针,不能调用_X _y
}


template <calsss T>
virtual T sub(T t1){//error
return _x - _y +t1;
}

private:
double _x;
double _y;
};

将成员函数模板的声明与实现分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Point
{
public:
Point(double x,double y)
: _x(x)
, _y(y)
{}

//定义一个成员函数模板
//将_x转换成目标类型
template <class T>
T convert()
{
return (T)_x;
}

template <class T>
T add(T t1);

private:
double _x;
double _y;
};

//类外实现成员函数模板时,需要加上模板声明
template <class T>
T Point::add(T t1){
return _x + _y + t1;
}

void test(){
Point pt2(6.5,7.7);

cout << pt2.add(10) << endl;
//T是int类型 所以结果是(int)(6.5+7.7+10)=24
}

类模板

形式:

1
2
3
4
template <class T >
class 类名{
//类定义...
};

类模板的使用

用类模板的方式实现一个stack类,可以存放任意类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <class T, int kCapacity = 10>
class Stack
{
public:
Stack()
: _top(-1)
, _data(new T[kCapacity]())
{
cout << "Stack()" << endl;
}
~Stack(){
if(_data){
delete [] _data;
_data = nullptr;
}
cout << "~Stack()" << endl;
}
bool empty() const;
bool full() const;
void push(const T &);
void pop();
T top();
private:
int _top;
T * _data;
};

类模板的成员函数如果放在类模板定义之外进行实现,需要注意

(1)需要带上template模板形参列表(如果有默认参数,此处不要写,写在声明时就够了)

(2)在添加作用域限定时需要写上完整的类名和模板实参列表

1
2
3
4
template <class T,int kcapacity>
T Stack<T,kcapacity>::top(){
//...
}

写成这样

1
2
3
template <class T = int, int kCapacity = 10>
class Stack
{};

可以隐式实例化吗》

1
2
3
4
void test(){
Stack st;//error 不能这样用,即使有默认值也需要加空的尖括号
Stack<> st;//successful
}

可变参数模板

可变参数模板(variadic templates)是 C++11 新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。

可变参数模板和普通模板语义上是一样的,只是写法上有一些区别

1
2
template <class ...Args>
void func(Args ...args);

Args叫模板参数包,相当于将T1/T2/T3等等类型参数打了包

args叫函数参数包,相当于将t1/t2/t3…等函数参数打了包

使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class ...Args>
void func(Args ...args){
cout << sizeof...(Args) << endl;//看一下模板参数包中到底打包了多少类型参数
cout << sizeof...(args) << endll;//看一下函数参数包中到底打包了多少个函数参数
}

void tese(){
func();
cout <<endl;

func(1,2.3,true);
cout << endl;

func(3.1,'a',3,false);
}

但这样是使用的参数包,如果解包呢?

1
2
3
4
5
6
7
8
9
10
11
12
void print(){
cout << endl;
}
template <class T,class ...Args>
void print(T x, Args ...args){
cout << x << " ";
print(agrs...);//省略号在右边, 继续往下解包,但是最后一次调用会出问题,一般把普通函数作为递归函数的出口就解决了最后一次调用的问题了,因为普通函数的优先级比函数模板的优先级更高
}

void test(){
pirnt(1,2.2,'c',"helol",5.6);
}

解决递归函数最后一次调用所处的问题的解决方案二(其实跟解决方案一样,不过只适用于最后一个参数类型是double)

1
2
3
4
5
6
7
8
void print(double x){
cout << endl;
}
template <class T,class ...Args>
void print(T x, Args ...args){
cout << x << " ";
print(agrs...);
}

如果不知道最后一种类型参数是什么呢?

1
2
3
4
5
6
7
8
9
template <class T>
void print(T x){
cout << x << endl;
}
template <class T,class ...Args>
void print(T x, Args ...args){
cout << x << " ";
print(agrs...);
}

推荐使用解决方案一,即使用普通函数作为出口函数

上面是一个参数一个参数的接,那两个两个的接行不行?

1
2
3
4
5
6
7
8
9
10
11
12
//所以需要这样一个出口函数
template <class T>
void print(T x){
cout << x << endl;
}

//两个两个的解,但这样没有合适的出口的话,传入的参数就必须是偶数个
template <class T1 class T2,class ...Args>
void print(T x,T2 y, Args ...args){
cout << x << y << " ";
print(agrs...);
}

——如果想要获取所有的参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
void printType(){
cout << endl;
}

//重新定义一个可变参数模板,至少得有一个参数
template <class T,class... Args>
void printType(T x, Args... args)
{
cout << typeid(x).name() << " ";
printType(args...);
}

printType(1,"hello",3.6,true,100);