模板
为什么要学习模板编程
在学习模板之前,一定要有算法及数据结构的基础,以及重载,封装,多态,继承的基础知识,不然会出现看不懂,或者学会了没办法使用。
为什么C++会有模板,来看下面的代码。
add()第一版
#include <iostream>
#include <string>
using namespace std;
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
string add(string a, string b) {
return a + b;
}
int main() {
cout << add(1, 2) << endl;
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
return 0;
}
当我们使用add函数时,不同的类型要去重载实现不同参数的add函数,那么有多少种相同类型进行相加,那么我们就要重载实现多少种add函数,那么就对于我们程序员来说这种方法就很麻烦,那么模板编程就可以帮我们避免这种麻烦。来看下面这段代码:
add()第二版
#include <iostream>
#include <string>
using namespace std;
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(1, 2) << endl;
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
return 0;
}
可以发现第一版我实现了3种add函数,而第二版我只实现了一种add函数,直接少写了很多重复的逻辑代码,这就是为什么需要学习模板编程。
模板编程(泛型编程)
程序 = 算法 + 数据结构
数据结构:可以存储任意类型
算法:能够操作存储任意类型数据的数据结构
例如vector容器它是能够存储任意类型的顺序表,sort函数可以对任意类型的顺序表进行排序并且还可以自定义排序规则,而这两个例子都是通过模板编程进行实现。
模板对于我们程序员来说是一种工具,而这种工具我们可以在程序设计中把任意类型进行抽象出来。
模板函数
例如文章开头的例子,我不知道add中需要传入的参数是什么,那么也不知道具体的返回值是什么,那么我们就需要利用模板编程进行抽象出来一个模板函数,进行可以对任意类型进行处理的函数。
那么我拿第二版的add函数进行继续探索模板函数,现在我有一个需求是
cout << add(1, 1.2) << endl
传入的参数是不同的,那么我该如何设计,如下:
#include <iostream>
#include <string>
using namespace std;
//template是用来引入模板参数的关键字
//typename先理解为定义一个任意类型的变量
template<typename T, typename U>
T add(T a, U b) {
return a + b;
}
int main() {
cout << add(1, 2) << endl;
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
cout << add(1, 1.2) << endl;// 结果 2
cout << add(1.2, 1) << endl;// 结果 2.2
return 0;
}
那我就加两个模板任意参数就可以了,这样就可以不发生报错了,但是又有一个问题了,我,我把add(1, 1.2)中的参数进行调换了位置,他的结果会不一样,因为我的返回值是T类型,那么对应的就是第一个参数的类型,如果第一个参数是1那么返回值类型就是int,第一个参数是1.2那么返回值类型就是float。
那么这里会引入一个新的关键字decltype
decltype:
这里我们用到的是第二点,也就是判断复杂表达式的结果是什么类型。
#include <iostream>
#include <string>
using namespace std;
//template是用来引入模板参数的关键字
//typename先理解为定义一个任意类型的变量
template<typename T, typename U>
//T和U是任意类型
//不管什么类型都有一个默认构造
decltype(T() + U()) add(T a, U b) {
return a + b;
}
int main() {
//int也有可以当作类来使用
//所以任意类型都有构造函数
int num = int(1);
cout << add(1, 2) << endl;
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
cout << add(1, 1.2) << endl;
cout << add(1.2, 1) << endl;
return 0;
}
问题又来了,如果我传入T类型,而这个T类型默认构造被删除了,那这个代码是会发生报错的,那么如何去解决呢,那么这里又引出了一个新的概念返回值后置:
这里会用到一个新的关键字auto
#include <iostream>
#include <string>
using namespace std;
//template是用来引入模板参数的关键字
//typename先理解为定义一个任意类型的变量
template<typename T, typename U>
//在原来返回值的位置写一个auto关键字
//auto是用来推到后置返回值的类型
//因为-> 在参数列表后面,根据代码的执行顺序,那么a和b就可以进行使用
//所以可以用decltype进行判断a + b的返回类型
//然后传给auto进行推断,最后确定返回值类型
auto add(T a, U b) -> decltype(a + b){
return a + b;
}
int main() {
//int也有可以当作类来使用
//所以任意类型都有构造函数
int num = int(1);
cout << add(1, 2) << endl;
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
cout << add(1, 1.2) << endl;
cout << add(1.2, 1) << endl;
return 0;
}
模板类:
在下面这份代码中,我利用模板实现了一个模板类,而这个模板类是一个简单对于数组的实现:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
//利用模板创建一个模板类
template<typename T>
class A {
public :
A(int n = 10) : n(n) {
this->arr = new T[n];
}
T &operator[](int ind) {
if (ind < 0 || ind > n) return __end;
return arr[ind];
}
void rand_arr() {
for (int i = 0; i < n; i++) {
int x = rand() % 100;
arr[i] = x - (T)x / 10;
}
return ;
}
~A() {
delete arr;
}
//在声明友元函数时,也要加上模板的关键字引入和模板参数
template<typename U>
friend ostream &operator<<(ostream &out, const A<U> &obj);
private :
T *arr;
int n;
T __end;
};
//重载输出时也需要利用到模板编程
template<typename T>
ostream &operator<<(ostream &out, const A<T> &obj) {
for (int i = 0; i < obj.n; i++) {
cout << obj.arr[i] << " ";
}
return out;
}
int main() {
srand(time(0));
//通过模板类创建对象时
//需要确定模板类型中的模板参数类型
A<int> a;
a.rand_arr();
A<double> b;
b.rand_arr();
cout << a << endl;
cout << b << endl;
return 0;
}
认识了大概的模板类进行如何使用我们继续往下探索:
模板特化与偏特化
模板特化
现在假如我对于add函数进行使用时,我需要对int类型特殊处理,也就是返回值结果需要加2,那么就需要用到模板函数的特化:
#include <iostream>
#include <string>
using namespace std;
//template是用来引入模板参数的关键字
//typename先理解为定义一个任意类型的变量
template<typename T, typename U>
//在原来返回值的位置写一个auto关键字
//auto是用来推到后置返回值的类型
//因为-> 在参数列表后面,根据代码的执行顺序,那么a和b就可以进行使用
//所以可以用decltype进行判断a + b的返回类型
//然后传给auto进行推断,最后确定返回值类型
auto add(T a, U b) -> decltype(a + b){
return a + b;
}
//由于是特化,那么我们就确定了参数
//就不需要传入模板参数了,但是也得需要template关键字引入
template<>
int add(int a, int b) {
return a + b + 2;
}
int main() {
//int也有可以当作类来使用
//所以任意类型都有构造函数
int num = int(1);
cout << add(1, 2) << endl; //那么这里的结果应该是 1 + 2 + 2 = 5
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
cout << add(1, 1.2) << endl;
cout << add(1.2, 1) << endl;
return 0;
}
模板偏特化
如下我传入的参数是指针类型时,我该如何进行处理
int a = 10, b = 20;
cout << add(&a, &b) << endl;
这里我们就会用到偏特化:
#include <iostream>
#include <string>
using namespace std;
//template是用来引入模板参数的关键字
//typename先理解为定义一个任意类型的变量
template<typename T, typename U>
//在原来返回值的位置写一个auto关键字
//auto是用来推到后置返回值的类型
//因为-> 在参数列表后面,根据代码的执行顺序,那么a和b就可以进行使用
//所以可以用decltype进行判断a + b的返回类型
//然后传给auto进行推断,最后确定返回值类型
auto add(T a, U b) -> decltype(a + b){
return a + b;
}
//由于是特化,那么我们就确定了参数
//就不需要传入模板参数了,但是也得需要template关键字引入
template<>
int add(int a, int b) {
return a + b + 2;
}
//模板偏特化版本
//当传入的参数是指针类型时的版本
template<typename T, typename U>
auto add(T *a, U *b) -> decltype(*a + *b) {
cout << "this is piantehua" << endl;
return *a + *b;
}
int main() {
//int也有可以当作类来使用
//所以任意类型都有构造函数
int num = int(1);
cout << add(1, 2) << endl; //那么这里的结果应该是 1 + 2 + 2 = 5
cout << add(2.1, 3.3) << endl;
string a = "hello", b = "world";
cout << add(a, b) << endl;
cout << add(1, 1.2) << endl;
cout << add(1.2, 1) << endl;
int c = 10, d = 20;
cout << add(&c, &d) << endl;
return 0;
}
可变参数模板
typename
现在来说一下typename的作用:
typename的作用就是,声明后面的表达式是一个类型。
可变参模板函数
现在我要实现一个函数叫做print,他可以打印所有的参数,并且参数的个数是任意的:
#include<iostream>
using namespace std;
//递归出口,打印最后一个参数
//也就是偏特化版本,只有一个参数时的print
template<typename T>
void print(T a) {
cout << a << endl;
return ;
}
//一个模板参数代表当前层数的参数的类型, ARGS代表后续跟着的参数的类型
template<typename T, typename ...ARGS>
void print(T a, ARGS ...args) {
//打印当前层的的第一个参数
cout << a << " ";
//递归到下一层,去打印下一个参数
print(args...);
return ;
}
int main() {
int a = 10;
print(a, 12.3, "hello", '0', "gg");
return 0;
}
那么最终只有一个参数时,会调用的print的偏特化版本只有一个参数时,进行递归结束。
可变参模板类
下面实现一个类,这个类模板的参数个数是不定的,并且演示了如何获取每一层中分别对应的变参类型:
#include<iostream>
using namespace std;
//template引入当前类型中的第一个参数T
//然后引入变参列表ARGS
template<typename T, typename ...ARGS>
class ARG {
public :
//将T类型重命名为getT
typedef T getT;
//将下个一个类重命名为next_T
typedef ARG<ARGS...> next_T;
};
//类的递归出口,只有一个参数时的模板类
template<typename T>
class ARG<T> {
public :
typedef T getT;
};
int main() {
//取到第一层中的int
ARG<int, double, long long, float>::getT a;
//取到第二层中的double
ARG<int, double, long long, float>::next_T::getT b;
//取到第三层中的long long
ARG<int, double, long long, float>::next_T::next_T::getT c;
//取到第四层中的float
ARG<int, double, long long, float>::next_T::next_T::next_T::getT e;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(e) << endl;
return 0;
}
下面通过上面的代码,在提出一个需求:
之前我取最后一层的类型需要这样去取,那如果我有10个,100个参数,就需要去写,
n - 1个next_T吗,所以我需要进行迭代更新一下:
ARG<int, double, float, char>::next_T::next_T::next_T::getT e;
改完之后:
ARG<3,int, double, float, char>::getT e;
数字3就代表我要取的对应的类型,那么如何实现看下面代码:
#include<iostream>
using namespace std;
//基础模板声明
//因为进行偏特化处理时或者特化处理时需要基础模板
template<int n, typename ...ARGS>
class ARG_imag;
//偏特化模板递归
template<int n, typename T, typename ...Rest>
class ARG_imag<n, T, Rest...>{
public :
//进行递归,直到找到需要的层数
typedef typename ARG_imag<n - 1, Rest...>::thisT thisT;
};
//偏特化模板递归出口
//当n等于0时,说明到达需求层数,进行递归结束
template<typename T, typename ...Rest>
class ARG_imag<0, T, Rest...> {
public :
typedef T thisT;
};
//进行封装
//用户调用的是ARG
template<int n, typename ...ARGS>
class ARG {
public :
typedef typename ARG_imag<n, ARGS...>::thisT getT;
};
int main() {
//取到第一层中的int
ARG<0, int, double, float, char>::getT a = 123;
//取到第二层中的double
ARG<1, int, double, float, char>::getT b = 12.3;
//取到第三层中的float
ARG<2, int, double, float, char>::getT c = 123.3;
//取到第四层中的char
ARG<3, int, double, float, char>::getT e = 'c';
cout << "sizeof(a) = " << sizeof(a) << " a = " << a << endl;
cout << "sizeof(b) = " << sizeof(b) << " b = " << b << endl;
cout << "sizeof(c) = " << sizeof(c) << " c = " << c << endl;
cout << "sizeof(e) = " << sizeof(e) << " e = " << e << endl;
return 0;
}
模板中的引用重叠
C++11 标准引入了 "引用折叠" 规则,这个规则定义了在模板实例化过程中,不同类型的引用组合如何被折叠成最终的引用类型。引用折叠规则如下:
- T & & -> T &
- T & && -> T &
- T && & -> T &
- T && && -> T &&
也就是说传入的类型是右值引用,并且参数中也是右值引用,T类型才是右值引用,否则是左值引用。
下面带入代码演示:
#include<iostream>
using namespace std;
#define TEST(func, n) {\
printf("%s(%s) ", #func, #n);\
func(n);\
}
template<typename T>
void func(T &&a) {
//假如T为int & 那么a的类型就为int & &&然后通过折叠得到为int &
if (is_same<T &, decltype(a)>::value) {
cout << " is left" << endl;
//假如T为int && 那么a的类型就为int&& &&然后通过折叠得到为int &&
} else if (is_same<T &&, decltype(a)>::value) {
cout << " is right" << endl;
} else {
cout << " is a type" << endl;
}
return ;
}
int main() {
int n = 123;
int& l = n;
int&& r = 123;
TEST(func, n); //n为左值, T类型就为int &
TEST(func, l); //l为左值, T类型就为int &
TEST(func, r); //r为左值, T类型就为int &
TEST(func, 123); //123为右值, T类型就为int &&
TEST(func, move(n)); //move(n)为右值, T类型就为int &&
return 0;
}
那么又有新问题出现了,传入的类型为引用的类型,那么该如何取获取他的类型呢,如下:
std::remove_reference
是一个类型特征工具,它能从一个类型中去除引用,并返回无引用的类型。例如:
-
对于
int&
,std::remove_reference<int&>::type
是int
。 -
对于
int&&
,std::remove_reference<int&&>::type
也是int
。
#include<iostream>
using namespace std;
template<typename T>
void func(T &&t) {
//通过remove_reference,去掉T的引用获取到他的类型
typedef typename remove_reference<T>::type a;
if (is_same<a, int>::value) cout << "a type is int" << endl;
if (is_same<a, char>::value) cout << "a type is char" << endl;
if (is_same<a, double>::value) cout << "a type is double" << endl;
if (is_same<a, float>::value) cout << "a type is float" << endl;
if (is_same<a, string>::value) cout << "a type is string" << endl;
}
int main() {
int a;
string str = "hello";
func(a);
func(str);
func('a');
func(3.14);
return 0;
}