目录
模板是什么?
模板格式
模板本质
函数模板
格式介绍
显式实例化
模板参数匹配原则
类模板
类模板的实例化
非类型模板参数
模板特化——概念
函数模板特化
类模板的特化
全特化
半特化
偏特化
三种类特化例子(放一起比较)
模板分离编译
STL中比较经典的模板应用(不包含argus)
容器适配器
仿函数
结语
模板是什么?
假设我们要写一个函数,这个函数的参数我们设置了两个int
但假如我现在在main函数里面调用的时候,我不光想传两个int,我想传一个int,一个double,再或者我想一个传double,一个传float
但是我的函数参数只写了两个int
void func(int i, int j)
{
cout << "hello world" << endl;
}
int main()
{
func(1, 2);
return 0;
}
hello world
想要解决这种情况只有两个方法:
- 每种参数的函数都写一遍
- 模板
什么是模板,模板就是我们自己当老板,让编译器帮我们打工
template<class T1, class T2>
void func(T1 i, T2 j)
{
cout << "hello world" << endl;
}
int main()
{
func(1, 2);
func(1.1, 2);
func(1.1, 2.2);
func(1.1, 'x');
return 0;
}
模板格式
我们要写模板的话,得按照如下格式:
template<class T1, class T2>
如果要加参数的话就在后面加,如果要加缺省值也可以,这个我们后面再讲(STL中的容器适配器就是一个例子)
当然我们也可以将class换成typename
template<typename T1, typename T2>
目前来讲,两者并没有区别,所以我们写class即可(单词少)
模板本质
模板的本质就是,我们写了一个类模板或是一个函数模板,在我们看来我们是只写了一份,但是编译器就会在我们编译之后,根据我们传的参数,在背后默默实现出多份
举个例子:
template<class T1, class T2>
void func(T1 i, T2 j)
{
cout << "hello world" << endl;
}
int main()
{
func(1, 2); //int int
func(1.1, 2); //double int
func(1.1, 'x'); //double char
return 0;
}
在我们眼里,这是一份
在编译器眼里,代码长这样:
void func(int i, int j)
{
cout << "hello world" << endl;
}
void func(double i, int j)
{
cout << "hello world" << endl;
}
void func(double i, char j)
{
cout << "hello world" << endl;
}
也就是说,编译器就是在背后默默打工,我们不苦,苦了编译器而已
函数模板
格式介绍
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
如上这就是我们的函数模板,我们写的模板参数T在函数里面可以直接当成类型去传
到时候我们传了什么参数给编译器,编译器就将T实例化成什么
但是这时我们会遇到一个问题,如果我模板参数只写了一个T
按理来说,我们传的应该就是两个一样的对象是吧
但是这时我就不,我就要传两个不一样的,我传一个int,一个double,那在编译器看来,就不知道你这个T想变成什么了
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, d2);
return 0;
}
显式实例化
像上面的代码,我们只有两个解决方法:
- 我们自己传过去的时候强转——a1, (int)d1
- 显示实例化
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
//方法一
Add(a1, (int)d2);
//方法二
Add<int>(a1, d2);
return 0;
}
我们可以看到,显示实例化就是在函数后面加一个尖括号,然后里面写的类型就是我们想让模板参数成为的类型
比如模板参数只有一个T,这时我显式实例化传了一个int,那么int就是T的类型
如果类型不匹配的话,编译器会尝试强转,如果强转不了,就报错
模板参数匹配原则
我们的模板也是有匹配原则的
比如我很喜欢吃牛肉,但是今天家里面没有牛肉,这时我吃一桶泡面一顿就勉强过去了是不是也可以
但是如果我家这时刚好有牛肉,那我是不是就不吃泡面了呀(假设只能二选一)
如果其没有牛肉,也没有方便面,只有你最讨厌的肥猪肉,你闻一下都感觉恶心,那这顿是不是就不在家里吃了,只能出去觅食了
编译器也是这样的,有最合适的模板,就用最合适的,如果没有合适的,强转一下也能用,那也行
要是根本就没有匹配的,那编译器就只能报错了
template<class T>
void Add(const T& left, const T& right)
{
cout << "T" << endl;
}
void Add(const int& left, const double& right)
{
cout << "int double" << endl;
}
int main()
{
Add(1, 1);
Add(1, 1.1);
return 0;
}
注意,在模板调用的时候,会优先调用非模板参数(如果匹配的话)
另外,一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
int Add(int left, int right)
{
cout << "1" << endl;
return left + right;
}
template<class T>
T Add(T left, T right)
{
cout << "2" << endl;
return left + right;
}
int main()
{
Add(1, 1);
Add<int>(1, 1);
return 0;
}
类模板
函数模板其实是一个大坑,稍有不注意的话,就会狠狠报错
相比之下,更多人会更愿意直接使用函数(非模板)
但是类模板就不一样了,这个可就太牛了
template<class T1, class T2, ..., class Tn>
class 类模板名
{
类内成员定义
};
如上是类模板的格式
template<class T>
class date
{
public:
date(T year, T month, T day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "类模板" << endl;
}
private:
T _year;
T _month;
T _day;
};
int main()
{
date<int> d1(1, 1, 1);
return 0;
}
注意,类模板是一定需要显示实例化的(除非有缺省值)
我们可以看到,在这个date类里面,我们将其显式实例化为int,所以里面的内容都会变成int
但是如果我们此时有这么一个需求:我们类里面的一些函数,我们不想在类里面实现,我想在类外面实现,因为这些函数太长了,我在外面实现,里面会简洁且美观
这时,我们就需要在函数前面加上类域限定,并且在函数上面我们还要加上类模板
如下(就拿上面date函数的析构来举例):
template<class T>
class date
{
public:
date(T year, T month, T day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "类模板" << endl;
}
~date();
private:
T _year;
T _month;
T _day;
};
template<class T>
date<T>::~date()
{
cout << "~date()" << endl;
}
类模板的实例化
vector<int> v1;
vector<double> v2;
vector<string> v3;
如上,我们类模板是需要显示实例化的,因为他不像函数模板那样子,可以根据传的参数去一定程度上判断,所以是必须要传的!!
我们需要记住的是,我们使用模板实现的类只是一个模具
而编译器则会根据这些模具实现出各种不同的类,也就是我们当老板,编译器当打工人
我们不苦,写一个类就好,编译器苦,编译器要在底层实现很多个类
非类型模板参数
模板参数分为两种:
- 类型形参
- 非类型形参
类型形参就是我们上面一直在写的 template<class T>
非类型形参就是一个常量传过去 template<class T, size_t N = 10>
值得注意的是,在C++20以前,浮点数,类对象,字符串这些,是不能作为非类型模板参数的
但是在C++20之后又支持了(不是所有编译器都支持C++20)(所以我们日常最好不要这么用)
模板特化——概念
我们在日常写代码的时候,会遇到一些情况,比如我们在使用堆(优先级队列)的时候,会用到仿函数,这时我们的标准库里的仿函数可能并不满足我们的要求,所以我们就需要自己再动手写一个
我们可以这么理解:特化就是在原模板类的基础上,针对特殊类型所进行特殊化的实现方式
比如,我在外面做手工艺品(小熊),一般情况下小熊的耳朵我都是涂的棕色,这时有一个客户过来说要白色的,我就只能”特化“一个白色耳朵的小熊给客户
函数模板特化
函数模板特化就是直接生成一个有指定需求的函数出来,如下:
struct Date
{
int year;
int month;
int day;
bool operator<(Date& d1)
{
return d1.year < year;
}
};
template<class T>
bool myless(T& left, T& right)
{
return left < right;
}
/函数模板的特化 举例
template<>
bool myless<Date*>(Date* & left, Date* & right)
{
return *left < *right;
}
特化有这么几个步骤:
- template后面的东西清空,留一对尖括号
- 在函数名后面加一对尖括号,里面写上要特化的类型
看着挺好的,但其实这是一个大坑啊!!!
template<class T>
bool myless(const T& left, const T& right)
{
return left < right;
}
template<>
bool myless<Date*>(Date* const & left, Date* const& right)
{
return *left < *right;
}
试想一下,如果我们加上了const呢?
上面的代码是正确的,但是大多数人在写的时候,会将const写到Date*的前面
但其实我们要想明白的一点是,我们const修饰的是指针本身,当我们类型为T的时候,修饰的就是T,但是如果是指针的话,如果const在*前面,那么修饰的就是指针所指向的值,如果在*后面的话,那么修饰的就是指针本身
类模板的特化
类模板的特化分为了几种:
- 全特化
- 偏特化(部分特化)——下文叫半特化
- 偏特化(进一步限制参数)
全特化
// 全特化
template<class T1, class T2>
struct Date
{
Date()
{
cout << "Date<T1, T2>" << endl;
}
};
template<>
struct Date<int, char>
{
Date()
{
cout << "Date<int, char>" << endl;
}
};
首先我们要知道的是,特化是需要原模板的,没有原模板就不能特化
首先我们来看一看全特化
全特化就是将所有的参数都限制死,就必须是这个类型才能调用这个特化,一般情况下都是拿来做特殊处理使用
半特化
顾名思义,半特化就是特化一半,另一半还是类模板参数,如下:
// 半特化
template<class T>
struct myless<T, int>
{
myless() { cout << "半特化" << endl; }
bool operator()(T& x, int& y)
{
return x < y;
}
};
我们可以看到,这里和全特化的区别就是,全特化是全固定死的,但是半特化这里是只有指定数量的是固定死的,另一部分就还是模板
偏特化
偏特化就比较特殊了,一般情况下用来表示一类数据
// 偏特化
template<class T1, class T2>
struct myless<T1*, T2*>
{
myless() { cout << "偏特化" << endl; }
// 此处的T类型不为T*,而是T
// 如果我此时传过来的是int*,则此时T的类型为int
bool operator()(T1* x, T2* y)
{
return *x < *y;
}
};
如上代码表示的是:只要你是指针类型,就走我这个特化
但是有一点需要注意,就是,我们上面特化的是T1*,T2*,这时假设我们传的是一个int*过去,这时我们的T就是int,而不是int*
这时因为如果T为int的话,我们就能通过自己控制来整出int对象和int*对象,但是如果T是int*的话就只能是指针了
我们将三者结合到一起来看一看
三种类特化例子(放一起比较)
// 原模版
template<class T1, class T2>
struct myless
{
myless() { cout << "原模版" << endl; }
bool operator()(T1& x, T2& y)
{
return x < y;
}
};
// 全特化
template<>
struct myless<char, double>
{
myless() { cout << "全特化" << endl; }
bool operator()(char& x, double& y)
{
return x < y;
}
};
// 半特化
template<class T>
struct myless<T, int>
{
myless() { cout << "半特化" << endl; }
bool operator()(T& x, int& y)
{
return x < y;
}
};
// 偏特化
template<class T1, class T2>
struct myless<T1*, T2*>
{
myless() { cout << "偏特化" << endl; }
// 此处的T类型不为T*,而是T
// 如果我此时传过来的是int*,则此时T的类型为int
bool operator()(T1* x, T2* y)
{
return *x < *y;
}
};
int main()
{
myless<int, char> ml;//原模版
myless<char, double> m2;//全特化
myless<int, int> m3;//半特化
myless<int*, int*> m4;//偏特化
myless<int**, int**> m5;//偏特化
return 0;
}
模板分离编译
模板分离编译,说简单点就是:我在.h文件里面声明了模板,但是在.cpp文件里面写出模板的定义
就是把模板声明的声明和定义分离到两个文件
这时候,大坑就来了
我们来看这么一个例子:
首先我们先创建三个文件:一个头文件(.h)两个.cpp文件
我们在.h文件里面声明了两个函数,一个是普通函数,一个是带模板的函数
然后我们在test.cpp这里调用这两个函数,但是我们会发现,报错了
只调用一个普通函数就不会
这就说明,编译器在模板函数声明定义分离的情况下,是找不到的
我们来简单分析一下:
那编译器为什么不编译模板呢???
只要编译器去编译模板,那就会生成地址,就能解决问题了
友友们,在我们的未来,我们要面对的,可能是成百上千个文件,每个文件可能都有成千上万行
编译器可以去一个一个文件地去找,这个模板的声明,对应的实例化在哪里,可以
但是这时,假设我们不分离编译的话,编个代码就几秒钟,但是一个一个文件去找的话,可能就需要半个小时了,这不夸张、
所以解决这个问题最好的办法就是,把模板的声明和定义写在同一个文件里,这样子编译器就能直接找到声明和定义,就能解决问题
STL中比较经典的模板应用(不包含argus)
容器适配器
这个我们在实现栈和队列,优先级队列的底层的时候会用到,这个其实就是:
将其他的数据结构当成一个模板参数传过来
template<class T, class container = vector<T>>
我们可以看到,上述代码中,我们将vector作为一个容器传给了模板作为参数,甚至我们还可以给缺省值,如果我们不传的话,就默认是vector,如果传的话,就以我们传的为准
如果有对容器适配器的具体应用感兴趣的话,可以看看下面这两篇文章:
一篇是栈和队列的底层实现,一篇是堆(优先级队列)的底层实现
【STL】| C++ 栈和队列(详解、deque(双端队列)介绍、容器适配器的初步引入)
【C++】STL | priority_queue 堆(优先级队列)详解(使用+底层实现)、仿函数的引入、容器适配器的使用
仿函数
这个在堆、AVL树、红黑树中都有用到
具体就是,我们可以写一个类模仿函数的行为,也就是在类里面重载一个operator()
然后我们就可以将这个类作为模板的其中一个参数,然后在模板所在的那个类里面调用仿函数对应的逻辑
template<class T>
struct myless
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
struct mygreater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T, class container = vector<T>, class compare = myless<T>>
如果对仿函数的应用较为感兴趣的话,同样可以看看下面这篇文章(是堆的底层实现)
【C++】STL | priority_queue 堆(优先级队列)详解(使用+底层实现)、仿函数的引入、容器适配器的使用
结语
到这里,我们这篇博客就结束啦!~( ̄▽ ̄)~*
如果感觉对你有帮助的话,希望可以多多支持博主喔!(○` 3′○)