C++模板进阶
模板是C++泛型编程的基础。在C++里,模板分为类模板和函数模板
非类型模板参数
模板参数分类类型形参与非类型形参。
- 类型形参 : 出现在模板参数列表中。跟在class或者typename之类的参数类型名称。
- 非类型形参 : 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
在C++11新增的array容器,就用到了非类型模板参数。
template<class T,size_t N> //这里的N必须是常量。
class Container
{
T* _arr;
int _capacity = N;
};
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
类模板的默认参数
类模板的默认参数使用起来和函数的默认参数一样。
template<class T,size_t N = 1000>
class Container
{
T* _arr;
int _capacity = N;
};
int main()
{
int i1;
Container<int> a1;
}
函数模板的特化
函数模板的特化步骤:
1.必须要先有一个基础的函数模板
2.关键字template后面接—对空的尖括号 <>
3.函数名后跟一对尖括号,尖括号中指定需要特化的类型
4.函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
模板的特化是指,对某些类型进行特殊化处理。
例如:
my_swap函数用于交换两个变量,但此时对于交换两个vector容器函数内部需要进行拷贝和赋值,效率太低。
template<class T>
void my_swap(T& x1, T& x2)
{
T tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
vector<int> v1 = { 1,2,3 };
vector<int> v2 = { 3,4,5 };
my_swap(v1, v2);
}
这里可以使用模板的特化,此时针对vector {int> 类型直接调用其swap函数,底层只交换了三个指针就完成了交换。
template<> void my_swap<vector<int>>(vector<int>& v1, vector<int>& v2)
{
v1.swap(v2);
}
类模板特化
全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "Data" << endl;
}
};
template<>
class Data<double, double>
{
public:
Data()
{
cout << "double,double" << endl;
}
};
int main()
{
Data<double,double> d1;
//打印double,double
}
偏特化
只要第二个模板参数匹配上,就走这个类模板。
//偏特化,半特化(特化部分)
template<class T1>
class Data<T1, int>
{
public:
Data()
{
cout << "T1,int" << endl;
}
};
int main()
{
Data<double,int> d1;
//打印T1,double
}
类模板不一定特化参数,而是对参数的进一步限制。
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "T1*,T2*" << endl;
}
};
int main()
{
Data<int*,int*> d1;
//打印T1*,T2*
}
分离编译
在大型项目开发中,有大量的h文件和.cpp文件,但模板在分离时存在许多问题。
func.h
#include <iostream>
template<class T>
void print();
func.cpp
#include "func.h"
template<class T>
void print()
{
std::cout << "hello world" << std::endl;
}
test.cpp
#include <iostream>
using namespace std;
#include "func.h"
int main()
{
print<int>();
}
此时编译会弹出链接错误。
在程序从源文件到可执行程序的链接阶段,调用函数时,需要往符号表中填入函数的物理地址(普通函数如果只有声明没有实现,编译时会报链接错误),如果是普通函数在这里是可以找到其物理地址,原因是在汇编阶段,普通函数就已生成了指令,而模板函数并没有生成。因为在链接阶段前,每个源文件都是独立的,模板函数根本不知道他是怎么被调用的,不知道其类型,它不可能为每个类型都生成一份函数,到了链接阶段,也就找不到其物理地址了。
在编译器实现的角度,它确实可以去其他文件找模板函数是怎么被调用的,从而确定其类型,并实例化,但这样肯定会大大的增加程序的编译时间,这种方案不现实。
解决方案:
方法1:
指定其类型,缺点是每个类型都要指定,不推荐。
func.h
#include <iostream>
template<class T>
void print();
//处理方法1
template
void print<int>();
方法2:
把声明和实现放在同一个文件里,并命名hpp文件。STL里就是用这种实现。
按需实例化
以下代码,print函数内有语法错误,但此时编译并不会显示编译错误,只有在调用print函数时,才会检查出来。(这是在vs2013的表现,在vs2019下是会检查出语法错误,19的检查更加严格)
template<class T1>
class Temp
{
void print(const T1& x);
private:
T1 a;
};
template<class T1>
void Temp<T1>::print(const T1& x)
{
a = x
}
模板总结
【优点】
1.模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
⒉.增强了代码的灵活性
【缺陷】
1.模板会导致代码膨胀问题,也会导致编译时间变长
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误