什么是泛型
了解模板之前,先了解一个概念:泛型。
泛型,实质上就是不使用具体数据类型(例如 int、double、float等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业务逻辑的实现。是一种将类型参数化进而达到代码复用的技术,强调的是,在C++中是使用模板来实现泛型。
通过需求,引出模板的作用
通常使用普通的函数实现一个与数据类型有关的算法是很繁琐的,比如两个数的加法,要考虑很多类型:
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
Point add(Point a, Point b)
{
return a + b; //这里面的加号是被运算符重载过的
}
在C++中可以通过函数重载来解决这个问题,但是反复写相同算法的函数是比较辛苦的,更重要的是函数重载是静态编译,运行时占用过多内存。在此我们可以用C++的模板函数来表达通用型的函数,如下:
template <typename T> // 模板声明
T add(T a, T b) // 类型参数化,T就是占位符,等待传入的参数是(int、double、Point)
{
return a + b;
}
其中:template < typename T> 是模板的声明,下面的T add(T a, T b) 就是模板函数了,T为待传入的类型参数,比如(int double Point)。这样的话,编译器会根据传入参数的类型,自动实现不同类型的函数运算,非常的高效便捷。
其中 typename 完全可以换成class,这两个是完全等价的
template <class T>
T add(T a, T b)
{
return a + b;
}
模板定义的规则
占位符T仅仅是一个符号
typename 后跟的“T”只是一个占位符而已,是自定义的,可以改成任意字符,只需要全部统一即可。但是,尽量统一,不要乱写符号,尽量写成T,因为T可以认为是Type的简称。
template <class ZMM>
ZMM add(ZMM a, ZMM b)
{
return a + b;
}
总之,我们这样定义,代表ZMM这种类型是可以发生变化的,可以接受任意类型参数,进而完成类似于函数重载的工作。
那么,代码中的 T 是什么呢?很明显,这是一个占位符,更确切的说是一个类型占位符。也就是说,将来在 T这个位置上的是一个真实、具体的数据类型,至于到底是哪个类型,完全取决于用户的需求。
当然,如果硬要给 T 这种类型占位符也叫做一种数据类型,提供这种想法的发明者称它为泛型(generic type),而使用这种类型占位符的编程方式就被称为泛型编程。
然后,我们在调用add函数时,就可以传入不同的类型,调用不同的函数:这里,调用函数并打印返回值
cout << add(1, 2) << endl;
cout << add(1.1, 2.2) << endl;
cout << add(Point(1,2), Point(2, 3)) << endl;
但,其实,完整的调用是这样的:
cout << add<int>(1, 2) << endl;
cout << add<double>(1.1, 2.2) << endl;
cout << add<Point>(Point(1,2), Point(2, 3)) << endl;
编译器很智能,根据传入参数自动判断类型是什么,所以才有了省略的写法。
我们如果想要程序更加正规,就采取完整的写法,比较清晰。知道完整调用之后,我想我们可以理解类型参数化是什么意思了,也就是将int/double/Point传递给参数T,然后生成一个新的函数。
多类型参数的模板写法
假如,我想要传进去的参数,一个是int类型,一个是double类型,返回的是float类型,如何实现呢?
template <typename T1, typename T2, typename T3> // 多类型参数
T3 add(T1 a, T2 b) // 不同类型修饰不同变量,要与自己需求对应准确
{
return a + b;
}
调用时:这种返回值类型和参数类型不同的情况,必须完整调用,即要在函数名后面完整的写清楚<类型参数>
add<int,double,float>(1, 2.12);//完整调用,明确参数都是什么
泛型的原理本质
我们通过传入不同的参数,达到了不同类型之间的计算问题。即,C++通过模板实现了泛型。那么泛型的原理是什么呢?
根据不同的参数类型,调用相同的模板函数?但我根据函数重载的原理,我大概猜测应该是编译器通过模板函数生成了三个不同的函数,让我们通过汇编看看情况是不是这样
可以看出,传入不同类型参数时,编译器会根据模板为其生成一个全新函数,供其调用。也就是在调用函数时,模板会立马生成一个。但是如果不调用函数,编译器就不会生成函数,因为都不知道你传入的参数是啥,编译器也不知道生成什么类型的函数。
也就意味着:我只有调用函数了,模板才会触发,才会生成对应的函数。就像类一样,只有创建对象才有类的存在,否则编译器根本不care。