目录
1.非类型模板参数
模板参数分为类型参数与非类型参数。
类型形数:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可以将该参数当成常量来使用。
非类型模板参数主要是用常量定义一些大小和固定值。下列通过非类型模板参数可以定义一个用静态数组作为顶层存储容器的栈,并且通过传入不同的整数可以改变栈的大小。
template<size_t N = 10> //可以给缺省值,可以写多个
class stack
{
public:
int _a[N];
int _top;
};
int main()
{
//两个stack类型
stack<> s0;
stack<5> s1;
stack<10> s2;
return 0;
}
注:(1)非类型形参可以给定缺省值,也可以在同一个模板中写多个非类型形参。(2)浮点数、类对象以及字符串是不允许作为非类型模板参数的(C++20支持使用double作为非类型模板参数) 。(3)非类型的模板参数必须在编译期就确定结果。
2.模板的特化
2.1概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现一个专门用于小于比较的函数模板
// 函数模板 -- 参数匹配
#include <iostream>
using namespace std;
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
Less在比较整型和日期类是都能得到正确的答案,但是在进行p1和p2比较时,比较的是指针的大小,指针在内存中存储的地方是随机的,所以可能会得到错误的答案。此时,就需要对模板进行特化。即:在原模版类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化和类模板特化。
2.2函数模板特化
函数模板的特化步骤:
(1)必须要先有一个基础的函数模板。
(2)关键字template后面接一对空的尖括号<>。
(3)函数名后跟一对尖括号,尖括号中指定需要特化的类型。
(4)函数形参表:必须要和模板函数的基础参数完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
return 0;
}
注:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将函数直接给出。
bool Less(Date* left, Date* right)
{
return *left < *right;
}
下面通过日期类的比较来更好的了解函数模板的特化:
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
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;
};
template<class T>
bool LessFunc(const T& left, const T& right)
{
return left < right;
}
//特化
template<>
bool LessFunc<Date*>(Date* const& left, Date* const& right) //这里希望修饰引用变量,不是修饰指针指向的内容,这个特化还不能给const Date*使用,会造成权限放大
{
return *left < *right;
}
template<>
bool LessFunc<const Date*>(const Date* const& left, const Date* const& right)
{
return *left < *right;
}
//推荐这么写
//直接写一个函数
bool LessFunc(const Date* left, const Date* right)
{
return *left < *right;
}
int main()
{
Date d1(2024, 8, 10);
Date d2(2024, 9, 10);
cout << LessFunc(d1, d2) << endl;
Date* p1 = &d1; //这里还是走的函数模板特化,没有走普通函数的类型转换进行权限缩小
Date* p2 = &d2;
cout << LessFunc(p1, p2) << endl;
const Date* p3 = &d1;
const Date* p4 = &d2;
cout << LessFunc(p3, p4) << endl;
}
如果想通过指针进行日期大小的比较的话,函数模板的特化需要特化一个普通的用于Date*的特化和一个const版本的,如果没有const版本的特化,则比较两个const修饰的Date*类型时还是走的原来的函数模板,这样就又会是结果不可控。
如上述代码,当两个特化的模板函数和进行Date*比较的函数同时存在时,对const对象进行比较时,如果要使用直接写的函数,会走一个类型转换进行权限缩小,所以编译器在这里还是选择了特化的模板函数中const的版本。
2.3类模板特化
类模板的特化步骤:
(1)必须要先有一个基础的类模板。
(2)关键字template后面接一对空的尖括号<>。
(3)类名后跟一对尖括号,尖括号中指定需要特化的类型。
2.3.1全特化
全特化即是将模板参数泪飙中所有的参数都确定化。
template <class T1, class T2>
class Data
{
public:
Data() { cout << "Data <T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//全特化
template <>
class Data<int, char>
{
public:
Data() { cout << "Data <int, char>" << endl; }
};
template <>
class Data<int, double>
{
public:
Data() { cout << "Data <int,double>" << endl; }
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
Data<int, double> d3;
return 0;
}
2.3.2偏特化/半特化
偏特化的时候要在特化的类模板的模板参数列表中写出没有特化的类型参数。
这里先给出一个原始的类模板:
template <class T1, class T2>
class Data
{
public:
Data() { cout << "Data <T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
偏特化有以下两种表现方式:
(1)部分特化:将模板参数类中的一部分参数特化。
template<class T1> //在类型形参列表中写出没有被特化的类型参数
class Data<T1, double>
{
public:
Data() { cout << "Data <T1, double>" << endl; }
};
int main()
{
Data<int, double> d1;
return 0;
}
(2)参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
a.偏特化指针:
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
};
b.偏特化引用:
template<typename T1, typename T2>
class Data<T1&, T2&>
{
public:
Data() { cout << "Data<T1&, T2&>" << endl; }
};
c.也可以指针和引用混着特化:
template<typename T1, typename T2>
class Data<T1&, T2*>
{
public:
Data() { cout << "Data<T1&, T2*>" << endl; }
};
int main()
{
Data<int, int> d1;
Data<int*, char*> d2;
Data<char&, double&> d3;
Data<char&, double*> d4;
return 0;
}
注:偏特化和全特化都匹配的时候,先选择全特化。
3.模板分离编译
3.1什么是分离编译
一个程序(项目)又若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译。
3.2模板的分离编译
假如有以下场景,模板的声明和定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
#include "a.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
c/c++程序要运行,一般要经历以下步骤:预处理 -> 编译 -> 汇编 -> 链接。编译是对程序按照语言特性进行词法,语法,语义分析,错误检查无误后生成汇编代码,注意头文件不参与编译,编译器对工程中的多个源文件是分离开单独编译的。 链接:将多个obj文件合成一个,并处理没有解决的地址问题。
为什么模板不能分离编译呢,因为将模板的实现放在a.cpp中定义实现时,在运行到Add(1, 2)时,需要向上去找Add()函数,但是在a.h中只有函数模板的声明,并没有定义,没有对模板的实例化,就不会产生函数的地址,当进行链接的时候,找不到函数地址,所以会报一个链接错误。
3.3解决方法
1.将声明和定义放到一个文件中,推荐使用这种方法。
2.在模板定义的位置显式实例化。
//a.cpp
#include "a.h"
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
template
int Add(const int& left, const int& right);
template
double Add(const double& left, const double& right);
下列代码是不在定义的位置显式实例化,在调用的时候显式的传类型参数,这样也不能解决这个问题.
#include"a.h"
int main()
{
Add<int>(1, 2);
Add<double>(1.0, 2.0);
return 0;
}
4.模板总结
优点:(1)模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此产生.(2)增强了代码的灵活性.
缺点:(1)模板会导致代码膨胀,也会导致编译时间变长.(2)出现模板编译错误时,错误消息非常凌乱,不易定位错误.