C++模板(模板的类型、模板的特化、可变参数模板)
模板(template)是 C++ 提供的一种具有高度灵活性的功能,它允许函数或类在编译时,由编译器去识别接口传入的参数的类型,从而实例化出对应的函数接口或类类型。 在以前如果函数或类类型想要支持多种参数, C 语言需要命名多种不同的接口,或者在编译命令中调整宏变量,C++ STL 库中,则需要手动重载多种参数类型的接口。但有了模板之后,C++ 就可以只写一份代码,便可在编译时生成任意参数类型的接口。
1. 模板的优缺点
优点:
- 模板复用代码,把实例化具体类型的工作交给编译器,节省资源,更快的迭代开发。
- 增强了代码的灵活性。
缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
- 模板是按需实例化,调用哪个成员函数就实例化哪个,如果代码有问题不一定能检查出来。
2. 模板的类型
2.1 函数模板
在全局中先声明模板的名称:template<typename T>
然后将代替函数的参数,编译器在调用函数传参时,会自动生成适用的函数。
template <typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
这个函数能够实现交互两个类型相同的变量的值
如果函数中传参的类型不同,只需多定义几个模板,编译器也会自动在调用函数时生成适用的函数
template <class T1,class T2>
double Add(T1& x, T2& y)
{
return x+y;
}
这个函数能够以double类型返回不同类型的两个变量相加的值;
2.2 类模板
类模板的格式
template<class T>
class c
{
public:
T* a;
int b;
double c;
};
int main()
{
c<int> c1;
c<double> c2;
return 0;
}
在这里c1中的a的类型是
int*
,c2中的a的类型是double*
2.3 非类型模板
非类型模板的参数是类型加常量,非类型模板的参数只支持整型家族(int、unsighed int、size_t、char),c++20支持其他类型的非类型模板参数。
template<class T,size_t N=10>
class arr
{
private:
T _a[N];
};
int main()
{
arr<int> a1;
arr<int, 100>a2;
return 0;
}
4. 模板实例化
4.1 隐式实例化
如果不特别声明,编译器会根据传参推算出参数类型。
4.2 显式实例化
函数模板在调用时可以手动声明指定生成的参数模板。
template<class T>
T Add(T x, T y)
{
return x+y;
}
int main()
{
int x=1;
int y=2;
cout<<Add<int>(x,y)<<endl;//显显式实例化手动声明
return 0;
}
类模板在调用时必须显式实例化,因为只有手动声明编译器才会知道需要生成什么类型的类
template<class T>
class c
{
public:
T* a;
int b;
double c;
};
int main()
{
c<int> c1;
c<double> c2;//必须显式实例化
return 0;
}
5. 模板的特化
模板的特化是用来针对某些特殊类型进行特殊处理的工具。特化可以分为全特化和半特化(偏特化)。
5.1 全特化
全特化的写法是写一个template<>
但<>
里面内容为空,接着将类的类型显示实例化。
template<class T,class R>
class A
{
private:
T _t;
R _r;
};
template<>
class A<int, char>
{
public:
A()
{
cout << "class A<int,char>" << endl;
}
};
int main()
{
A<int, char> a1;
return 0;
}
5.2 半特化
半特化可以是特化时保留一部分的模板参数,实例化一部分模板参数,但半特化不一定是部分特化参数,可能是对模板进一步限制。
保留部分模板参数:
template<class T,class R>
class A
{
public:
T _t;
R _r;
};
template<class T>
class A<T, char>
{
public:
A()
{
cout << "class A<T,char>" << endl;
}
};
对参数进一步限制:
template<class T1,class T2>
class A
{
public:
T1 _t;
T2 _r;
};
template<class T1,class T2>
class A<T1*, T2*>
{
public:
A()
{
cout << "class A<T1*,T2*>" << endl;
}
};
template<class T1, class T2>
class A<T1&, T2&>
{
public:
A()
{
cout << "class A<T1&,T2&>" << endl;
}
};
int main()
{
A<int*, char*> a1; //注意不是生成了一个int**和char**的类型,而是对int*和char*这个类型半特化
A<int&, char&> a2;
return 0;
}
6. 模板的特点
如果有一个现成的函数和一个符合条件的模板,编译器会优先调用现成的函数
int Add(int x,int y)
{
return x+y;
}
template<class T>
T Add(T x, T y)
{
return x+y;
}
int main()
{
int x=1;
int y=2;
cout<<Add(x,y)<<endl;//这里调用现成的函数
return 0;
}
编译器会优先调用 int Add(int x,int y)
但是如果对Add()
适用显式实例化,即使有现成适用的函数,编译器也会强制用模板生成一个新的函数来调用
int Add(int x,int y)
{
return x+y;
}
template<class T>
T Add(T x, T y)
{
return x+y;
}
int main()
{
int x=1;
int y=2;
cout<<Add<int>(x,y)<<endl;//这里调用的是模板
return 0;
}
7. 可变参数模板
7.1 可变模板参数介绍
c语言有可变参数函数(如printf()
),c++11将可变参数扩展到了模板上,称为可变参数模板。可变参数模板会在编译阶段解析并展开可变参数的类型,其最终的样貌仍是一般的函数。可变模板参数在使用时...
的位置比较多变,需要注意。
声明方式:
template<class ...Args>
void Print(Args ...args)
{
;//……
}
使用sizeof来统计可变参数包的参数数量:
但需要注意的是,sizeof在使用在可变参数模板上时,不可用于运行时方式。即不可在for循环中使用sizeof...(args)
来作为判断循环的条件。
#include<iostream>
using namespace std;
template<class ...Args>
void Count(Args ...args)
{
cout<<sizeof...(args);//注意...的位置
}
int main()
{
Count(1, 2.2, "hello world");
return 0;
}
7.2 可变参数模板的应用
可变参数模板可以用于将多个变量传入一个一次只操作部分参数的函数,其调用过程类似于递归。
#include<iostream>
using namespace std;
void _Print()
{
;
}
template<class T, class ...Args>
void _Print(const T& t, Args...args)
{
cout << t << endl;
_Print(args...);
}
template<class ...Args>
void Print(Args ...args)
{
_Print(args...);
}
int main()
{
Print(1, 2.2, "hello world");
return 0;
}
在代码中,首先调用
Print()
,再调用内部的_Print()
。_Print()
每次调用,可变参数模板包的参数就被T拿走一个,直到可变参数模板包中的参数为 0(可变模板参数可以是 0),就去调用重载的 0 参数的_Print()
,来结束调用。
可变模板参数在容器上的使用:
C++11为 STL 的容器提供了 emplace()
和 emplace_back()
两个接口来同时插入多个数据(这两个函数同时也支持了左值和右值的插入重载)。但要注意 emplace_back()
不是用来实现类似 initializer_list()
的功能的,它的用途是代替 pair
直接进入底层构造。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<pair<int,string>> v;
v.emplace_back(1, "one");
v.emplace_back(2, "two");
v.emplace_back(3, "three");
for (auto e : v)
{
cout << e.first << ":" << e.second << endl;
}
return 0;
}
emplace_back()
和 push_back()
的区别是,empalce()
可以做到直接构造,push_back()
只能做到先构造再拷贝构造。按上面的例子,empalce()
直接进入了 vector 的构造函数,对 val
成员变量初始化。但 push_back()
需要先将参数转换成 pair 类型,再进入vector 的构造函数初始化。
8. 使用模板可能会产生的错误
8.1 多模板适用造成的歧义错误
如果使用模板来写类的迭代器时,在调用这个迭代器的时候,如果没有显示实例化,编译器就会产生歧义:
#include <iostream>
using namespace std;
template<class T>
class A
{
public:
typedef T* iterator;
iterator begin()
{
//;
}
private:
T a;
};
template<class T>
void print(const T& a)
{
A<T>::iterator it = a.begin();
}
int main()
{
return 0;
}
产生这个错误的原因是编译器不确定这个东西的类型,因为程序编译到这里的时候,模板还没有实例化。编译器区分不出这是类型还是变量。当编译器报错
....意外标记"...."
一般都是这种原因产生的报错。
解决方法是,在代码前加一个
typename
告诉编译器这是一个类型而不是变量。这也是使用typename
和class
关键字声明模板的一个不同点。
8.2 模板声明和定义分离造成的链接错误
模板本质上是把实例化的类型的工作交给编译器,这个过程是在编译的时候完成的。但含有模板定义的文件和模板实例化的文件是在链接的时候拼在一起的。
具体就是编译器在编译模板实例化的代码时,在头文件只找到了声明没有找到地址,于是暂时跳过了查找定义继续编译。另一个文件中编译器编译到该模板时,不知道要把模板实例化成什么类型,于是也直接往下编译。所以,模板的实例化工作没有正常完成,当程序的翻译走到链接这一步的时候,对象找不到类的地址,于是报告了链接错误。
解决方法:
-
在定义文件对模板进行显示实例化,但缺点是必须提前确定好要实例化哪种类型,不符合泛性编程。
//.h文件 template<class T> class A { //………… }; //定义文件 template class A<int>; template class<char>;
-
将含有模板的部分保留在头文件里,这是最优的解决方法,缺点是头文件看起来不够简洁。