1.非类型模板参数
1.1引入
想定义一个静态的栈,该怎么写?
cpp
//定义一个静态的栈
#define N 10
template<class T>
class Stack
{
private:
T _a[N];
};
int main()
{
Stack<int,10> st1;
Stack<int,10000> st2;
return 0;
}
那如果想让两个栈的空间大小不同,又该怎么做呢?
cpp
//定义一个静态的栈
#define N 10000
template<class T>
class Stack
{
private:
T _a[N];
};
int main()
{
Stack<int,10> st1; //期望10
Stack<int,10000> st2; //期望10000
//只能把N改为10000,但这太不合适了
return 0;
}
针对此类问题,C++给了一种更好的解决方式,叫做非类型模板参数
1.2应用
对静态栈这个类进行改造
cpp
//之前写的模板参数叫做类型模板参数,比如T
//此时的N就是非类型模板参数
template<class T,size_t N>
class Stack
{
private:
T _a[N];
};
int main()
{
Stack<int,10> st1; //期望10
Stack<int,10000> st2; //期望10000
return 0;
}
1.3特点
非类型模板参数还有个特点,它是一个常量
也只有常量才能去定义数组的大小
虽然它是一个常量,但我们可以灵活控制这个整形的大小
我们传一个常量就可以控制,不需要像 #define N 10 一样是写死的,可以在实例化时传
模板参数可以允许去传一个常量
cppint main() { int n; cin >> n; Stack<int, n> st3; //注意:传变量是不行的 //为什么? //编译时要去实例化,如果是变量实例化时是不知道N是多少的,也就无法确定数组开多大 //注意:目前,非类型模板参数只支持整型,哪怕是浮点型都是不行的 //char是可以的,因为它也是整型 return 0; }
1.4array
在C++11中,出现了一个新容器------array,使用了非类型模板参数
cpp
int main()
{
array<int, 10> a1;
//它对比的是C语言的数组
int a2[10];
cout << sizeof(a1) << endl;
cout << sizeof(a1) << endl;
//二者大小相同,都是40,说明二者物理空间大小占用都是一样的
//它是和对象保持一致的,对象在栈它就在栈,对象在堆就在堆
//比如对象是new出来的,就是在堆上
return 0;
}
1.4.1优势
cpp
int main()
{
array<int, 10> a1;
int a2[10];
//C语言的数组有个大问题:
a2[15] = 1;
//在很多编译器上,比如VS2019,这里不会报错,但实际上是出了问题的
//因为数组的越界检查是一种抽查,所以没有检查出来
//这里本质是数组的解引用,会转换成指针的访问去访问第15个位置
//也没有办法检查,只能去设置标志位
//VS2022可以在通过语法编译时去强制识别,但场景稍复杂些就不行了
//array的优势:
a1[15] = 1;
//array可以检查出来
//因为这里本质是一个函数调用,operator()
return 0;
}
1.4.2实际使用
事实上,array的设计十分鸡肋
cpp
int main()
{
array<int, 10> a1;//不会去初始化
int a2[10];//不会去初始化
//C++11的array,设计初衷是期望使用者去替代静态数组
//但实际上,array设计的十分鸡肋
vector<int> v(10, 0);//这样写不是更好吗?
return 0;
}
2.类模板特化
2.1引入
cpp
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<int, int> d1;
//不管传递什么类型,都会去调用上面那个构造函数
return 0;
}
模板特化:想针对某些类型进行特殊化处理时,就可以使用特化
cpp
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 模板特化:针对某些类型进行特殊化处理
// 针对它是int和double类型时,进行特殊化处理
template<>
class Data<int,double>
{
public:
Data() { cout << "Data<int, double>" << endl; }
};
//我们把这个类叫做上面那个类的特化,也就是特殊化
//此时类型是int、douoble会执行下面这个类,不是的话才会去实例化上面那个类
int main()
{
Data<int, int> d1;
Data<int, double> d2;
return 0;
}
2.2应用
cpp
class Date
{
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);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
struct PDateCompare
{
bool operator()(Date* p1, Date* p2)
{
return *p1 < *p2;
}
};
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
//新的解决方法:
//特化一下,当T是Date*时,进行特殊处理,按指向的对象比较
template<>
class Less<Date*>
{
public:
bool operator()(Date* x, Date* y)
{
return *x < *y;
}
};
int main()
{
jxy::priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;//2018-10-30
//jxy::priority_queue<Date*, vector<Date*>, PDateCompare> q2;
//之前的解决方案是显式传了一个仿函数,使得可以比较日期而不是比较指针
//q2.push(new Date(2018, 10, 29));
//q2.push(new Date(2018, 10, 28));
//q2.push(new Date(2018, 10, 30));
//cout << *(q2.top()) << endl;
//那如果不想显式传仿函数,该怎么办呢?
jxy::priority_queue<Date*> q2;
q2.push(new Date(2018, 10, 29));
q2.push(new Date(2018, 10, 28));
q2.push(new Date(2018, 10, 30));
cout << *(q2.top()) << endl;
return 0;
}
详情见上一篇博客:优先级队列
2.3分类
模板特化 分为全特化 和偏特化。
cpp
//原模版
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
2.3.1全特化
全特化:即将模板参数列表中所有的参数都确定化。所有的模板参数都特化为一个具体的类型。
cpp
//------全特化
template<>
class Data<int, double>
{
public:
Data() { cout << "Data<int, double>" << endl; }
};
注意: 特化是不能单独存在的,必须要先有原模版 ,特化才可以存在
2.3.2偏特化
偏特化:针对任何模版参数,进行进一步的条件限制,而设计出的特化版本。
偏特化有以下 两种表现方式:
2.3.2.1部分特化
部分特化:将模板参数类表中的一部分参数特化。
cpp
//------偏特化、半特化:特化部分参数
template<class T1>
class Data<T1, double>
{
public:
Data() { cout << "Data<T1, double>" << endl; }
};
注意:特化之间可能会有重叠,在匹配时会遵循最匹配原则
cpp
int main()
{
Data<int, int> d1; //匹配原模版
Data<int, double> d2; //匹配全特化,遵循最匹配原则
Data<double, double> d3;//匹配偏特化
return 0;
}
2.3.2.2对参数更进一步的限制
偏特化并不仅仅是指特化部分参数 ,更是针对模板参数进行更进一步的条件限制,所设计出来的一个特化版本。
1.两个参数偏特化为指针类型
cpp
//------更特殊、花哨的偏特化
//这里甚至没有指定具体是什么类型
//但是它指示了:如果是指针,就执行该模板
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
关于指针的比较,即2.2应用 ,可以进行进一步的改造
cpp
//进一步的改造
//此时只要是指针,就会按照指向的对象去比较
template<class T>
class Less<T*>
{
public:
bool operator()(T* x, T* y)
{
return *x < *y;
}
};
2.两个参数偏特化为引用类型
cpp
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data()
{
cout << "Data<T1&, T2&>" << endl;
}
};
int main()
{
Data<int&, int&> d5;
return 0;
}
综上,偏特化的本质 ,其实就是对参数的进一步限制 ,比如限制参数是int类型 ,或是限制参数必须是指针、引用等。
3.函数模板特化
3.1引入
cpp
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
Date* d1 = new Date(2024, 10, 13);
Date* d2 = new Date(2024, 10, 10);
cout << Less(d1, d2) << endl;
//此时答案又是不确定的,时而是1,时而是0
return 0;
}
3.2应用
cpp
//函数模板特化
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//如果这里是Date*时,想要按照Date去比较
//所以要特化一下
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
//类模板特化,特化在类名的后面
//函数模板特化,特化在函数名的后面
int main()
{
Date* d1 = new Date(2024, 10, 13);
Date* d2 = new Date(2024, 10, 10);
cout << Less(d1, d2) << endl;
//此时答案就是确定的了
return 0;
}
3.3实际使用
事实上,类模板建议使用特化,但函数模板是不建议使用特化的
cpp
//template<class T>
//bool Less(T left, T right)
//正常情况下,这里不会这么写,传值会有拷贝
template<class T>
bool Less(const T& left, const T& right)//通常是这样写的
{
return left < right;
}
//想要针对类型是Date*时,进行特殊处理
//就要写特化,一般会把T直接替换成Date*
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{
return *left < *right;
}
int main()
{
Date* d1 = new Date(2024, 10, 13);
Date* d2 = new Date(2024, 10, 10);
cout << Less(d1, d2) << endl;
return 0;
}
3.3.1正确写法
cpp
template<class T>
bool Less(const T& left, const T& right)//通常是这样写的
{
return left < right;
}
//这样写才能解决问题
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{
return *left < *right;
}
int main()
{
Date* d1 = new Date(2024, 10, 13);
Date* d2 = new Date(2024, 10, 10);
cout << Less(d1, d2) << endl;
return 0;
}
3.4解决方案
为了避免上述这种情况的发生,想对某些类型进行特殊处理时,函数模板可以不使用特化
cpp
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
//写一个具体版本的函数就可以
//利用了编译器的匹配原则
//上面的模板和该函数严格来说不构成重载
//编译器编译时没有上面的模板代码
//模板不实例化,是没有的
bool Less(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
Date* d1 = new Date(2024, 10, 13);
Date* d2 = new Date(2024, 10, 10);
cout << Less(d1, d2) << endl;
cout << Less(1, 2) << endl;
//此时具体的函数才会和模板实例化后的函数构成重载
//模板会推演、实例化生成一个函数
return 0;
}
4.关于模板的分离编译
4.1何为分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件又会单独编译,生成目标文件,最后将所有目标文件链接起来,形成单一的可执行文件的过程,称为分离编译模式。
4.2模板不支持分离编译
模板不支持分离编译
而普通函数分离编译,是没有问题的
这是为什么?
4.3原因
这就是模板不支持分离编译的原因。
总结:主要问题还是,Stack.cpp中Add没有实例化。可以认为这就像是一种沟通不畅,
Stack.cpp中有Add的定义,但是它不知道要实例化成什么,所以自然没有函数地址
而Test.cpp中,知道Add的参数要实例化成什么,但是它只有声明,没有定义
4.4解决方案
4.4.1显式实例化
Stack.h文件
cpp
#pragma once
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
void func();
Stack.cpp文件
cpp
#include"Stack.h"
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
void func()
{
cout << "void func()" << endl;
}
//显式实例化:
//既然定义时不知道要实例化成什么,那就写出来
template
int Add<int>(const int& left, const int& right);
//所以说模板不是不能分离编译,只是需要显式实例化
Test.cpp文件
cpp
#include"Stack.h"
int main()
{
Add(1, 2);
func();
return 0;
}
4.4.1.1新的问题
Test.cpp文件
cpp
#include"Stack.h"
int main()
{
Add(1, 2);
func();
//但是这种方法有个很大的缺陷:
Add(1.1, 2.2); //call Add<double>(?)
//此时又会出现链接错误
return 0;
}
此时就需要在Stack.cpp文件中再写一段实例化的代码
cpp
#include"Stack.h"
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
void func()
{
cout << "void func()" << endl;
}
template
int Add<int>(const int& left, const int& right);
template
double Add<double>(const double& left, const double& right);
所以显式实例化这个方法虽然可行,但是并不好用,
一旦有新的类型去实例化,就需要去显式实例化这个新的类型,否则又会报错
4.4.2关于类模板的分离编译
之前使用的一直是函数模板,接下来我们来看看类模板的分离编译问题
Stack.h文件
cpp
#pragma once
#include<iostream>
using namespace std;
//类模板
template<class T>
class Stack
{
public:
void Push(const T& x);
void Pop();
private:
T* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
Stack.cpp文件
cpp
#include"Stack.h"
template<class T>
void Stack<T>::Push(const T& x)
{
cout << "void Stack<T>::Push(const T& x)" << endl;
}
template<class T>
void Stack<T>::Pop()
{
cout << "void Stack<T>::Pop()" << endl;
}
//类模板的解决方法可以更加暴力一些
//不需要一个个函数来实例化,可以直接把整个类给实例化
template
class Stack<int>;
但还是同样的问题,一旦有新的类型去实例化,就需要去显式实例化这个新的类型,否则又会报错
所以显式实例化 并不是一个长久之计
Test.cpp文件
cpp
#include"Stack.h"
int main()
{
Stack<int> st;
st.Push(1);
st.Pop();
//与上面的函数模板问题相同
Stack<double> st1;
st1.Push(1.1);
st1.Pop();
return 0;
}
4.4.3分离在同一个文件中
最合适的解决方案是,不要Stack.cpp文件 ,如果声明和定义要分离,把它们分离在同一个文件中,也就是Stack.h文件
此时可以把Stack.h文件 的名字改为Stack.hpp文件 ,可以更好地说明该头文件中不止有模板的声明 ,还有模板的定义,说明文件中有模板,不方便分离在两个文件中。
Stack.hpp文件
cpp
#pragma once
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
void func();
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
void func()
{
cout << "void func()" << endl;
}
//类模板
template<class T>
class Stack
{
public:
void Push(const T& x);
void Pop();
private:
T* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
template<class T>
void Stack<T>::Push(const T& x)
{
cout << "void Stack<T>::Push(const T& x)" << endl;
}
template<class T>
void Stack<T>::Pop()
{
cout << "void Stack<T>::Pop()" << endl;
}
还有一种解决方法就是,不让声明与定义分离,直接定义。
4.5补充
未来有没有可能去支持分离编译模板?
答案是不会的。模板本身就会增长编译的时间,因为它多了一个实例化的步骤。在编译阶段,都是单对单的处理,每个源文件都会分离、单独编译,也因此Add不会实例化,只有在最后的链接阶段,才会进行合并。
如果支持分离编译的话,就意味着在编译阶段 ,如果看到了有模板的存在,就要去把其它所有的文件都找一遍,寻找哪里在使用这个模板,实例化成什么,才能去支持分离编译,但是这样会大大地增长编译的时间
编译器编译时只会向上查找,就是为了提高编译速度
5.关于模板的总结
5.1优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
5.2缺点
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误