终于接触模板了。之前看到模板,泛型,都是模模糊糊,只知其一不知其二,现在好好捋顺。而且泛型总给我一种很玄妙的感觉,哈哈哈
模板自然也是C++在C基础上加的
其实并不玄妙,反而是一个存在地极其合理的好工具。
C++发展初期,很多很多人都不看好模板,没想到函数模板以及模板类后来会这么强大有用。
但是也有一群程序员挑战模板技术的极限,阐述了各种可能。
正是熟悉模板的程序员们的反馈帮助C++98添加了标准模板库。
函数模板使用泛型定义函数,而不是具体类型,所以函数模板是通用的函数描述,模板有时候也被称为通用编程。
也就是说,模板并不创建任何函数,只是告诉编译器该怎么定义函数。这一点上,函数模板很像结构声明,结构声明也叫结构描述,结构模板的嘛,他也不创建具体的结构,只是告诉编译器创建结构的方案。
泛型可以用任意的具体类型替换,比如int, double。
把类型作为参数传给模板,以使得编译器生成该类型的函数(类似于文本替换)。由于类型用参数表示,所以模板特性也被称为参数化类型 parameterized types。
模板常被放在头文件里面,需要使用就包含这个头文件。可见模板确实和文本替换有关系,有点像宏。
关键字:相同算法,不同类型
你想交换两个整数,于是你写了一个函数,两个参数类型是int &,返回类型是void
但是你又想交换两个double,于是你把刚才的代码复制下来,把int &都改成double &。
你又想交换两个char ,于是你又复制一遍刚才的代码,把int &改为char &。
·······
难道这就是你的编程生活?涂涂改改,缝缝补补的?是不是一点都感受不到编程之美了?你肯定也闻到了这个办法浓浓的蹩脚气息。你得想办法解决这个大痛点呀,毕竟光这么复制不仅显得愚笨,还费时间,替换修改还容易出错,更重要的是,增加了函数的数量,每个函数编译时都是要正经八百地申请内存居住的呀,如果我用到其中一个函数的机会其实并不多,那岂不是浪费内存吗?
所以C++ 针对这一痛点,祭出了模板特性,自动生成多个函数的定义,简单,省时,还可靠。
但是!不能缩短程序,即增加函数数量从而使用更多内存的痛点还是不能解决,因为使用模板编译出来的机器代码没有函数模板,只有具体的实际函数的代码。
之前学字符串的时候见过两个模板类,vector和array类。知道调用模板的时候要用尖括号把具体类型括起来,原来就是把类型传递给模板呢。
现在看看怎么编写模板的定义
template <typename T>//告诉编译器要建立一个模板,并把类型命名为T,也可以命名为AnyType等名称,满足标识符规则就行
//描述算法,即对T类型的参数做的操作
//可以看到,算法是完全一样的,不一样的只是放进来的参数类型而已
void myswap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
需要说明的是,以前(C++98添加关键字typename之前),创建模板用的关键字是class。到现在,也许仍然有大量的代码库使用了class关键字建立模板,看到的时候要知道是什么意思哦。在模板中,用typename和class是等价的,当然自己写模板就要用typename,毕竟是新标准,而且也很明确说明是类型名。
注意,函数模板的原型是:
template <typename T>
void myswap(T &a, T &b);
另外,并不是说函数模板的所有参数都得是泛型,也可以是具体类型,后面会有示例。
很简单
编译器会根据传入的参数类型自己生成一个正确类型的函数,真牛
#include
template <typename T>
void myswap(T & a, T & b);
int main()
{
using std::cout;
int i = 10, j = 30;
cout << "i = " << i << ", j = " << j << '\n';
myswap(i, j);
cout << "Using compiler-generated int swapper:\n";
cout << "Now i = " << i << ", j = " << j << "\n\n";
double x = 10.2, y = 30.1;
cout << "x = " << x << ", y = " << y << '\n';
myswap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x = " << x << ", y = " << y << "\n\n";
char m = 'x', n = 'y';
cout << "m = " << m << ", n = " << n << '\n';
myswap(m, n);
cout << "Using compiler-generated char swapper:\n";
cout << "Now m = " << m << ", n = " << n << '\n';
return 0;
}
template <typename T>
void myswap(T &a, T & b)
{
T temp;
temp = a;
a = b;
b = temp;
}
i = 10, j = 30
Using compiler-generated int swapper:
Now i = 30, j = 10
x = 10.2, y = 30.1
Using compiler-generated double swapper:
Now x = 30.1, y = 10.2
m = x, n = y
Using compiler-generated char swapper:
Now m = y, n = x
刚才说了,模板的关键是:同一算法,不同类型。
那要是不同类型做同一件事情的时候,算法不完全一样呢、
比如上面的示例,我要是传入两个数组,或者两个结构,用三变量交换法不能正常交换
那就像重载普通函数那样,重载模板,即两个同名模板的特征标(参数列表)不一样
并且我还把显示数组元素的函数也定义为了一个模板,学以致用了
#include
template <typename T>
void myswap(T & a, T & b);
template <typename T>
void myswap(T a[], T b[], int n = 5);//两个数组;模板的参数不一定都是泛型
template <typename T>
void showArray(T ar[], int len);
const int arSize = 5;
int main()
{
using std::cout;
int i = 10, j = 30;
cout << "i = " << i << ", j = " << j << '\n';
myswap(i, j);
cout << "Using compiler-generated int swapper:\n";
cout << "Now i = " << i << ", j = " << j << "\n\n";
double x = 10.2, y = 30.1;
cout << "x = " << x << ", y = " << y << '\n';
myswap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x = " << x << ", y = " << y << "\n\n";
char m = 'x', n = 'y';
cout << "m = " << m << ", n = " << n << '\n';
myswap(m, n);
cout << "Using compiler-generated char swapper:\n";
cout << "Now m = " << m << ", n = " << n << "\n\n";
int a[arSize] = {1, 2, 3, 4, 5};
int b[arSize] = {10, 20, 30, 40, 50};
cout << "Original array:\n";
cout << "a: ";
showArray(a, arSize);
cout << "b: ";
showArray(b, arSize);
myswap(a, b, 3);
cout << "New array:\n";
cout << "a: ";
showArray(a, arSize);
cout << "b: ";
showArray(b, arSize);
cout << '\n';
cout << "Original array:\n";
cout << "a: ";
showArray(a, arSize);
cout << "b: ";
showArray(b, arSize);
myswap(a, b);
cout << "New array:\n";
cout << "a: ";
showArray(a, arSize);
cout << "b: ";
showArray(b, arSize);
return 0;
}
template <typename T>
void myswap(T &a, T & b)
{
//这个代码是可以交换两个结构的哦,因为C++允许把一个结构赋值给另一个,但是不允许数组之间赋值
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void myswap(T a[], T b[], int n)
{
//注意a,b都是指针!
//虽然下面用数组表示法,但是可以修改参数
T temp;
int i;
for (i = 0; i < n; ++i)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
template <typename T>
void showArray(T ar[], int len)
{
int i;
for ( i = 0; i < len; ++i)
std::cout << ar[i] << ' ';
std::cout << std::endl;
}
i = 10, j = 30
Using compiler-generated int swapper:
Now i = 30, j = 10
x = 10.2, y = 30.1
Using compiler-generated double swapper:
Now x = 30.1, y = 10.2
m = x, n = y
Using compiler-generated char swapper:
Now m = y, n = x
Original array:
a: 1 2 3 4 5
b: 10 20 30 40 50
New array:
a: 10 20 30 4 5
b: 1 2 3 40 50
Original array:
a: 10 20 30 4 5
b: 1 2 3 40 50
New array:
a: 1 2 3 40 50
b: 10 20 30 4 5
局限性不是缺点,只是他做不到的事情。
就像上面这个示例展示的,我想交换两个数组的元素,之前写的第一个函数就不行了。
再比如,我在函数模板里写了a = b;,那么如果传入的是数组类型,那么赋值会报错;
如果我在函数模板里写了if (a > b),如果传入的是数组或者结构,都不行,因为数组名实际是地址,这其实是在比较地址大小,但这可能并不是我们想要的结果。
如果在函数模板里定义了a * b,那么传入指针,结构, 数组都不行。
总之,函数模板提供的通用化并不是一夫当关万夫莫开,一般数组,结构,指针等复合类型很容易就把关打开了。
怎么办呢?
不知道为啥,这一点看的我特别难受,感觉很看不下去,不过看懂了觉得也不难,可能是学习过度了····
显示具体化就是说,对一个模板,他如果不适用于某个类型,那我就用这个类型去具体化这一个模板,即专门写一个函数用于这个类型,但是这个函数仍然是这个模板的一部分。
一个函数名可以有普通函数,显式具体化的函数模板,普通函数模板以及这三者的重载版本。
执行程序时,编译器按照顺序选择对应原型的函数来调用:如果找得到适合的普通函数就用普通函数,否则找显式具体化的函数模板,最后找普通函数模板。
#include
struct job
{
char name[20];
float salary;
unsigned int floor;
};
//普通函数模板
template <typename T>
void myswap(T & a, T & b);
//显式具体化函数模板
//以template <>打头,中的job可以省略,因为参数列表也携带了类型信息
template <> void myswap<job>(job &, job &);
//常规函数
void myswap(job &, job &);
const int arSize = 5;
void showJob(job &a);
int main()
{
using std::cout;
int i = 10, j = 30;
cout << "i = " << i << ", j = " << j << '\n';
myswap(i, j);
cout << "Using compiler-generated int swapper:\n";
cout << "Now i = " << i << ", j = " << j << "\n\n";
double x = 10.2, y = 30.1;
cout << "x = " << x << ", y = " << y << '\n';
myswap(x, y);
cout << "Using compiler-generated double swapper:\n";
cout << "Now x = " << x << ", y = " << y << "\n\n";
char m = 'x', n = 'y';
cout << "m = " << m << ", n = " << n << '\n';
myswap(m, n);
cout << "Using compiler-generated char swapper:\n";
cout << "Now m = " << m << ", n = " << n << "\n\n";
job sue = {"Susan Yaffee", 4562.85, 7};
job sia = {"Sia Taylor", 5269.8, 9};
cout << "Before job swapping:\n";
showJob(sue);
showJob(sia);
myswap(sue, sia);
cout << "After job swapping:\n";
showJob(sue);
showJob(sia);
return 0;
}
template <typename T>
void myswap(T &a, T & b)
{
T temp;
std::cout << "Using template void myswap(T &a, T & b) \n" ;
temp = a;
a = b;
b = temp;
}
template <> void myswap(job & a, job &b)
{
//显式具体化模板和常规函数的代码是一毛一样的哈
float t1;
unsigned int t2;
std::cout << "Using template <> void myswap(job & a, job &b) \n";
t1 = a.salary;
a.salary = b.salary;
b.salary = t1;
t2 = a.floor;
a.floor = b.floor;
b.floor = t2;
}
void myswap(job &a, job & b)
{
float t1;
unsigned int t2;
std::cout << "Using void myswap(job &a, job & b) \n";
t1 = a.salary;
a.salary = b.salary;
b.salary = t1;
t2 = a.floor;
a.floor = b.floor;
b.floor = t2;
}
void showJob(job &a)
{
std::cout << a.name << ": $" << a.salary << " on floor " << a.floor << '\n';
}
可以看到,有常规函数时交换job类型使用的就是常规函数,而上面交换Int,double,char由于没有常规函数和显式具体化模板都使用了常规函数模板。
i = 10, j = 30
Using template <typename T> void myswap(T &a, T & b)
Using compiler-generated int swapper:
Now i = 30, j = 10
x = 10.2, y = 30.1
Using template <typename T> void myswap(T &a, T & b)
Using compiler-generated double swapper:
Now x = 30.1, y = 10.2
m = x, n = y
Using template <typename T> void myswap(T &a, T & b)
Using compiler-generated char swapper:
Now m = y, n = x
Before job swapping:
Susan Yaffee: $4562.85 on floor 7
Sia Taylor: $5269.8 on floor 9
Using void myswap(job &a, job & b)
After job swapping:
Susan Yaffee: $5269.8 on floor 9
Sia Taylor: $4562.85 on floor 7
把常规函数的定义和原型删掉,则程序选择了显式具体化模板,可见确实是上面所说的顺序
Before job swapping:
Susan Yaffee: $4562.85 on floor 7
Sia Taylor: $5269.8 on floor 9
Using template <> void myswap(job & a, job &b)
After job swapping:
Susan Yaffee: $5269.8 on floor 9
Sia Taylor: $4562.85 on floor 7
这里介绍几个概念或者术语。为了扎实的基础,应该把这些概念弄明白,才能进一步了解模板。
别晕了
隐式实例化,显式实例化,隐式具体化统称为具体化。
他们都是函数定义,使用了具体的类型,他们不是通用描述。
说过了,函数模板不是函数定义,而是指导编译器生成函数定义的方案。那么编译器生成的函数定义就是函数模板的一个实例。
注意实例化都是放在函数内部的,毕竟是生成实例嘛,当然在函数内部。而隐式具体化是放在所有函数外部的,和原型放在一起。
隐式实例化就是编译器根据参数的类型自动生成模板的实例,比如前面的示例中myswap(i, j),由于i,j是int类型,就直接实例 化了一个参数为int的函数定义。
最初只允许隐式实例化
template void myswap<int>(int, int);
还是要用关键字template和尖括号,并必须在尖括号写上要实例化的类型。
#include
template <typename T>
T add(T a, T b);
int main()
{
int m = 7;
double x = 3.4;
//x是double,m是int,和模板不匹配,但是函数名后面尖括号的double
//把函数强制为double实例化,并把参数m强制转换为double类型
std::cout << add<double>(x, m) << '\n';//显式实例化
return 0;
}
template <typename T>
T add(T a, T b)
{
return a + b;
}
10.4
但是这么做一定是可以类型转换成功才行,像下面这个例子,int无法转换为double &,就不行了
#include
template <typename T>
void myswap(T &a, T & b);
int main()
{
int m = 7;
double x = 3.4;
//myswap(x, m);//报错,因为double&不能指向int,转换不了,所以这个例子不行
std::cout << x << '\n';//显式实例化
return 0;
}
template <typename T>
void myswap(T &a, T & b)
{
T temp;
temp = a;
a = b;
b = temp;
}
上面已经说过了,显式具体化用于在模板的代码不适合某种类型时具体化,写一个适合这个类型的函数,但仍用模板关键字。
显式具体化的声明:(template <>打头)
template <> void myswap<int>(int &, int &);
template <> void myswap(int &, int &);//可以省略第二个尖括号
注意显式具体化在template关键字后面有一对空的尖括号。
显式具体化的函数原型都是有自己的函数定义的,编译器不会用myswap模板生成函数定义。
重载解析是指有函数重载,或者函数模板,或者函数模板重载的时候,编译器决定到底给每个函数调用使用哪一个函数定义。
这是一个很复杂的事情。大致来说:
两个函数模板,重载,看编译器选择最匹配的一个
#include
//重载模板
template <typename T>
void showArray(T arr[], int n);
template <typename T>
void showArray(T * arr[], int n);//指针数组,每个元素都是指向T的指针
int main()
{
int a[5] = {1, 2, 3, 4, 5};
showArray(a, 5);//T被替换为int
double *b[4];//指针数组
double c[4] = {2.1, 5.1, 8.1, 11.1};
for (int i = 0; i < 4; ++i)
b[i] = &c[i];
showArray(c, 4);
showArray(b, 4);
return 0;
}
template <typename T>
void showArray(T arr[], int n)
{
std::cout << "template A" << '\n';
for (int i = 0; i < n; ++i)
std::cout << arr[i] << ' ';//arr是指针,这里用数组表示法
std::cout << '\n';
}
template <typename T>
void showArray(T * arr[], int n)
{
std::cout << "template B" << '\n';
for (int i = 0; i < n; ++i)
std::cout << *arr[i] << ' ';//arr[i]是指针
std::cout << '\n';
}
template A
1 2 3 4 5
template A
2.1 5.1 8.1 11.1
template B
2.1 5.1 8.1 11.1
如果没有模板B,则第三次调用回合模板A匹配,T被double *代替,但是打印的就是地址了
template A
1 2 3 4 5
template A
2.1 5.1 8.1 11.1
template A
0x6dfeb8 0x6dfec0 0x6dfec8 0x6dfed0
#include
template <typename T>
T lesser(T a, T b);
int lesser(int a, int b);//非模板函数
int main()
{
int m = 20, n = -40;
double x = 34.5, y = -23.4;
std::cout << lesser(m, n) << '\n';//非模板函数
std::cout << lesser(x, y) << '\n';//模板函数,隐式实例化
std::cout << lesser<>(m, n) << '\n';//用尖括号让编译器选择模板函数,并隐式实例化
std::cout << lesser<int>(x, y) << '\n';//让编译器使用函数模板,并且在函数中显式实例化化为int类型
return 0;
}
template <typename T>
T lesser(T a, T b)
{
return (a < b) ? a : b;
}
int lesser(int a, int b)
{
//这是一个错误的函数。为了实验而已
a = (a < 0) ? -a : a;
b = (b < 0) ? -b : b;
return (a < b) ? a : b;
}
20
-23.4
-40
-23
这个关键字在C++11增加,增加他肯定是为了解决什么痛点咯,痛点就是,函数模板中有的数据的类型无法在编写模板时就确定,比如
template <typename T1, typename T2>
void ft(T1 a, T2 b)
{
?type? x = a + b;// 不知道给x声明什么类型!!!
...
}
如果a是double,b是int,则x是double
a是short,b是int,则x是int
总之写模板的时候是不能确定的
C++98没有解决这个问题,所以C++11新增了decltype关键字,但是这个关键字也没有完全地解决这个问题:它没法解决函数模板的返回值不确定的情况。
给decltype的参数是一个表达式。
template <typename T1, typename T2>
void ft(T1 a, T2 b)
{
decltype(a + b) x = a + b;// x的类型是表达式a+b的类型
...
}
下面详细说一下这个关键字决定类型的步骤:
必须按照顺序来,如果已经满足了,就不会再往后查询,这4步并不是平等的。
double x = 2.3;
double & y = x;
const double *P = x;
decltype(x) w;//w是double类型
decltype(y) u;//u是double &类型
decltype(p) z;//z是const double *
long ft(int);
decltype(ft(3)) x;//x是long类型
如果这个函数也有一堆重载的话,编译器大概也是像刚才说的那样去选择一个最佳匹配的函数定义,然后获得返回值类型
double x = 2.3;
decltype(x) y;//y是double类型,因为这是第一种情况
decltype((x)) z;//z是double &
int x = 2;
int & a = x;
int & b = x;
decltype(x + 2) w;//int
decltype(x + 2.1) u;//double
decltype(100L) z;//long
decltype(a + b) p;//p是int!!不是int &,虽然a, b是int &,但是加起来的值仍然是int哦
template <typename T1, typename T2>
void ft(T1 a, T2 b)
{
typedef decltype(a + b) T3;
T3 x = a + b;// x的类型是表达式a+b的类型
T3 c[4];
T3 &d = x;
...
}
decltype解决了函数模板内部的变量类型不确定的痛点,但是返回值类型不确定就没招。
于是C++11再次发力,拿出了后置返回类型,一种新的声明函数的方式。要利用auto关键字作为占位符。并在函数原型的参数后方写后置返回类型(trailing return type)。
这是C++11给auto新增的使用方法,如:
auto ft(int x, double y) -> float;
//还可以和decltype合作
auto ft1(int x, float y) ->decltype(x + y);//由于x,y写在参数列表后面,所以在作用域里,可以用
素来听闻坊间传言,说是C++东西太多太灵活容易出错,之前不以为然,今日一见,果真名不虚传。我把这个后置返回类型和显式实例化一起用,结果就错了
但我出错不能怪C++,只怪自己技艺肤浅,ft(x, y)应该传入两个类型才对,因为模板里用的是两个类型
#include
template <typename T1, typename T2>
auto ft(T1 x, T2 y) ->decltype(x + y);
int main()
{
int m = 20, n = -40;
double x = 34.5, y = -23.4;
std::cout << ft(m, n) << '\n';//模板函数,隐式实例化
std::cout << ft(x, y) << '\n';//模板函数,隐式实例化
std::cout << ft<>(m, n) << '\n';//让编译器选择模板函数,隐式实例化
std::cout << ft<int>(x, y) << '\n';//让编译器使用函数模板,并且在函数中显式实例化化为int类型
return 0;
}
template <typename T1, typename T2>
auto ft(T1 x, T2 y) ->decltype(x + y)
{
return x + y;
}
前三个输出都是对的,但是第四个输出了如此奇怪的结果,我通过调试发现,原来尖括号的int只把T1替换了!!T2仍然是隐式实例化为double ,所以得到了34-23.4=10.6
-20
11.1
-20
10.6
把最后后一个调用改为指定两个类型的显式实例化,就对了
std::cout << ft<int, int>(x, y) << '\n';
-20
11.1
-20
11
一直对模板感到很好奇,没想到在学习类之前先学了一波模板,果然是接触到了新鲜气息,难倒不是很难,就是知识点比较多,需要多练习才能掌握好,感觉到了用处,但是我的好奇心还有很多没被满足,想看更多模板真正散发魅力的高光时刻。