10--C++模板参数与特化详解
模板参数类似于函数参数, 只不过函数参数传的是对象, 而模板参数传的是类型而已.
1. 非类型模板参数
1.1. 非类型模板参数 基本介绍
模板参数简单介绍:
- 类型模板参数
- 非类型模板参数: 把常量值作为参数传入模板, 编译器生成不同的类
- 仅支持整形做非类型模板参数(CPP20之后支持其他类型)
- 编译时传参, 函数是运行时传参
下面来重点讲讲非类型模板参数。
- 本质:把常量值作为参数传入模板,编译器生成不同的类,在
CPP99
只支持整形,CPP11以后支持其他类型。 - 传参:常量值传参是在编译时进行传参的,因为涉及到开空间,要在运行前确定出来。
1.2. 非类型模板参数的典型应用——array
STL在CPP11时候引入了一个新的容器——array
,该容器是一个固定大小的数组。该容器就是用到了非类型模板参数进行实现,因为要传入常数明确常数大小开固定空间。
- 优势:相比于之前C语言中的数组,越界检查更加严格。
- 然而,相比于vector,可以说并没有多少进步。并且容易栈溢出.
#pragma once#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;namespace bit
{// 只支持整形做非类型模板参数// 非类型模板参数 类型 常量// 类型模板参数 class 类型template<class T, size_t N = 10>class array{public:size_t size() const;private:T _array[N];size_t _size = 0;};// 模板定义不建议分离到.cpp// 建议声明和定义都放到.htemplate<class T, size_t N>size_t array<T, N>::size() const{T x = 0;x += N;return _size;}void func();
}
array比静态数组强的一点是越界检查更加严格, 使得代码更加安全.
然而array不如vector:
- array与vector,初始化
array
没有初始化功能,而vector
完全可以在构造时候进行初始化。
array<int,10> a;//这里官方没有提供初始化构造函数哈,不是我刻意不用
vector<int> v(10, 1);cout << "array:" << endl;
for (auto& e : a)
{cout << e << " ";
} // -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460
cout << endl;cout << "vector:" << endl;
for (auto& e : v)
{cout << e << " ";
} // 1 1 1 1 1 1 1 1 1 1
cout << endl;
- array与vector,栈溢出
array更容易栈溢出。
array是模拟的是C语言中的静态数组,是在栈上开辟空间的,一般栈比较小。
vector是在堆上开辟空间的。这就说明,vector能开出的空间而array开不出来,因为栈远远小于堆的大小。
//array<int, 1000000> a;//代码为 -1073741571
vector<int> v(1000000, 1);//代码为 0
- array与vector,越界检查
array<int, 10> a;
vector<int> v(10, 1);for (int i = 0; i < 11; i++)
{a[i] = i;
}
//断言报错for (int i = 0; i < 11; i++)
{v[i] = i;
}
//断言报错
两者都是断言报错。
容器 | 初始化 | 栈溢出风险 | 越界检查 |
array | 不提供 | 风险高 | 断言 |
vector | 提供初始化 | 几乎不可能 | 断言 |
2. 模板的按需实例化
- 模板实例化:编译器不会直接编译模板,而是先根据你给的类型生成对应的实例化类,再对类进行编译。
- 模板的按需实例化:编译器对于要实例化的类不会完全全部实例化,编译器只会挑出用到的进行实例化。
显然,我们发现下图中的size调用是错误的,然而在没有调用[]时候编译器并不报错。
//szg::array<int, 100> a;//代码为 0
szg::array<int, 100> a;
a[0] = 0;//直接语法报错
3. 模板的特化
模板特化:在模板实例化的时候 针对某种/类的类型进行特殊处理/对待。
- 不符合条件的走模板
- 符合的类型走特化
- 全特化
- 半特化
3.1. 函数模板特化支持对类模板进行特殊类型处理
class Date
{
public:friend ostream& operator<<(ostream& _cout, const Date& d);Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}
private:int _year;int _month;int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}// -----------------------------------------------------------------------------------
template<class T> // 函数模板
bool Less(T left, T right)
{return left < right;
}
//bool Less(Date* left, Date* right)//函数重载也能表现为特化,不过本质是一种重载
//{
// return *left < *right;
//}
template<> // 函数模板特化
bool Less(Date* left, Date* right)
{return *left < *right;
}void test5()
{//一般类型的比较cout << Less(1, 2) << endl; // 1//自定义类型的比较cout << Less(Date(1, 1, 1), Date(2, 2, 2)) << endl; // 1//自定义类型指针的比较cout << Less(new Date(1, 1, 1), new Date(2, 2, 2)) << endl; // 1,这个地方走的是特化
}
注意:对于函数模板的特定类型特化,也可以使用函数重载进行特化,虽然本质上属于重载,但是效果与特化一样。
3.2. 函数模板特化也支持模板写法
template<class T>//函数模板
bool Less(T left, T right)
{return left < right;
}template<class T> // 函数模板特化
bool Less(T* left, T* right)
{return *left < *right;
}//template<>//函数模板特化
//bool Less(Date* left, Date* right)
//{
// return *left < *right;
//}void test5()
{//一般类型的比较cout << Less(1, 2) << endl;//1//自定义类型的比较cout << Less(Date(1, 1, 1), Date(2, 2, 2)) << endl;//1//自定义类型指针的比较cout << Less(new Date(1, 1, 1), new Date(2, 2, 2)) << endl;//1,这个地方走的是特化}
实际上,我感觉这个模板+特化就是写了一个更合适的模板匹配而已。
建议使用重载的方法进行特化,因为模板特化比较复杂。
3.3. 类模板特化
特化分为全特化和半特化,下面来进行举例说明。
// 类模板
template<class T1, class T2>
class A
{
private:T1 _a;T2 _b;
public:A(){cout << "template<class T1, class T2>" << endl;}
};// 全特化类模板
template<>
class A<double, double>
{
private:double _a;double _b;
public:A(){cout << "class A<double, double>" << endl;}
};//类模板的半特化
template<class T1>
class A<T1, int>
{
private:T1 _a;int _b;
public:A(){cout << "class A<T1, int>" << endl;}
};
// 类模板的半特化,不一定是特化一半参数,这里代表对类型进行限制都称为半特化。// 类模板的半特化
template<class T1, class T2>
class A<T1*, T2*>
{
private:T1 _a;int _b;
public:A(){cout << "class A<T1*, T2*>" << endl;}
};// 类模板的半特化
template<class T1, class T2>
class Data<T1&, T2*>
{
public:Data() { cout << "Data<T1&, T2*>" << endl; }
};void test6()
{A<char, char> a1;//函数模板template<class T1, class T2>A<double, double> a2;//全特化class A<double, double>A<double, int> a3;//半特化class A<T1, int>A<double*, int*> a4;//半特化class A<T1*, T2*>
}
3.4. 类模板特化应用
priority_queue<Date, vector<Date>, greater<Date>> pq;Date d1(2024, 4, 8);
pq.push(d1);
pq.push(Date(2024, 4, 10));
pq.push({ 2024, 2, 15 });while (!pq.empty())
{cout << pq.top() << " ";pq.pop();
}
cout << endl;priority_queue<Date*, vector<Date*>> pqptr;
pqptr.push(new Date(2024, 4, 14));
pqptr.push(new Date(2024, 4, 11));
pqptr.push(new Date(2024, 4, 15));while (!pqptr.empty())
{cout << *(pqptr.top()) << " ";pqptr.pop();
} // 结果随机,主要是因为比较的是new出来的地址。
cout << endl;
为了解决上面问题,我们可以
class GreaterPDate
{
public:bool operator()(const Date* p1, const Date* p2){return *p1 > *p2;}
};void test_priority_queue2()
{priority_queue<Date, vector<Date>, greater<Date>> pq;Date d1(2024, 4, 8);pq.push(d1);pq.push(Date(2024, 4, 10));pq.push({ 2024, 2, 15 });while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;priority_queue<Date*, vector<Date*>, GreaterPDate<Date>> pqptr;pqptr.push(new Date(2024, 4, 14));pqptr.push(new Date(2024, 4, 11));pqptr.push(new Date(2024, 4, 15));while (!pqptr.empty()){cout << *(pqptr.top()) << " ";pqptr.pop();}//加上仿函数之后,结果是唯一的。cout << endl;
}
template<class T>
class GreaterPDate<T*> // 右边这个T*就是进行匹配的,传的是指针的时候就会进行匹配该特化模板类
{
public:bool operator()(const T* p1, const T* p2){return *p1 > *p2;}
};void test_priority_queue2()
{priority_queue<Date, vector<Date>, greater<Date>> pq;Date d1(2024, 4, 8);pq.push(d1);pq.push(Date(2024, 4, 10));pq.push({ 2024, 2, 15 });while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;priority_queue<Date*, vector<Date*>, GreaterPDate<Date>> pqptr;pqptr.push(new Date(2024, 4, 14));pqptr.push(new Date(2024, 4, 11));pqptr.push(new Date(2024, 4, 15));while (!pqptr.empty()){cout << *(pqptr.top()) << " ";pqptr.pop();}//加上仿函数之后,结果是唯一的。cout << endl;
}
4. 模板分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
然而,由于模板按需实例化特点,这种分离编译模式可能会产生意想不到的连接错误。
4.1. 模板分离链接错误
下面展示经典分离错误
#include"array.h"void test7()
{bit::array<int, 10> a;cout << a.size() << endl; // 无法解析的外部符号 "public: unsigned __int64 __cdecl bit::array<int,10>::size(void)const "
}
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;namespace bit
{// 只支持整形做非类型模板参数// 非类型模板参数 类型 常量// 类型模板参数 class 类型template<class T, size_t N = 10>class array{public:size_t size() const; //声明,是模板private:T _array[N];size_t _size = 0;};void func();//声明,但是不是模板
}
#include"array.h"void bit::func()
{cout << "func()" << endl;
}namespace bit
{ template<class T, size_t N>size_t array<T, N>::size() const{return 1;}
} // 第一种写法//template<class T, size_t N>
//size_t bit::array<T, N>::size() const
//{
// return 1;
//}//第二种写法
作为对比,我们array.h里还实现了一个func函数进行分离
#include"array.h"
void test7()
{bit::array<int, 10> a;//cout << a.size() << endl;//无法解析的外部符号 "public: unsigned __int64 __cdecl bit::array<int,10>::size(void)const "bit::func();//func()
}
我们发现能够正常跑,这是为什么呢?
- 定义的地方,不知道实例化T成什么类型,所以有定义无法实例化,也就是无法生成函数的地址到符号表
- 调用的地方,知道T需要实例化成什么类型,但是因为编译阶段.cpp分离,没办法把T类型给到array.cpp从而造成编译器没办法为size()实例化出来。
如何解决?
4.2. 解决方法
如果执意把模板声明与定义分离,那么你可以在.h文件显示实例化,告诉编译器你定义的函数模板要实例化成什么类型。
4.2.1. 分离,显示实例化
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;namespace bit
{// 只支持整形做非类型模板参数// 非类型模板参数 类型 常量// 类型模板参数 class 类型template<class T, size_t N = 10>class array{public:size_t size() const; //声明,是模板private:T _array[N];size_t _size = 0;};void func();//声明,但是不是模板//显示实例化templateclass array<int>;templateclass array<double>;
}
#include"array.h"
void test7()
{bit::array<int, 10> a;cout << a.size() << endl;//1bit::func();//func()
}
缺点:必须挨个手动显示实例化...比较麻烦
4.2.2. 声明与定义不分离,放在同一个.h文件中
为什么模板的定义放到.h就不会出链接错误了
因为.h预处理展开后,实例化模板时,既有声明也有定义,直接就实例化
编译时,有函数的定义,直接就有地址,不需要链接时再去找
建议:强烈建议对于模板声明与定义要放在同一个文件中,省得麻烦。
5. 模板的优缺点
模板特性 | 概括 | 解释说明 |
优点 | 代码复用 | 把重复的类生成工作交给了编译器,对于模板使用掌握较好的程序员无疑提高了工作效率 |
缺点 | 编译时间会变长 | 因为我们把类生成过程交给了编译器,总体的工作量增加了, 增加在编译器实例化模板的这个过程需要额外开销, 但是程序员的工作量变小了, 因为程序员把一部分工作交给编译器了. 总结来说, 就是总体的工作量变大, 程序员的工作量变小了, 然后编译器的工作量变大了. |
不容易定位错误 | 编译器对模板信息掌握不全,很难帮助我们程序员去定位错误,有时会出莫名其妙的语法报错 |
说白了, 模板本质上属于一种代码复用, 并且这种复用一般是解决代码逻辑一致, 但类型不同的一种代码复用.