C++ Template 基础篇(一):函数模板_函数模板的定义及使用-CSDN博客
这篇博客提到模板是泛型编程的基础,把类型也当做参数,这样使得静态类型语言对类型的处理更强大,提高了代码的可重用性,目标和软件工程一致,提供可复用的代码
本章可以学到下面内容:
Function Templates
函数模板本身不是函数,它是一种“食谱”或者“图纸”,表示一系列的函数声明;模板是参数化的函数定义,函数的实例需要有参数才可以创建,也就是说非必要时不会生成函数定义
由函数模板生成函数的过程叫作实例化instantiation,模板的参数通常是类型,但也有可能是其他一些值,比如维度等
我们来看一个之前提到过的例子,函数模板语法大致如上,尖括号里是模板参数,然后把这里声明的参数当做正常类型使用即可
这里的T叫做模板类型参数,我们可以用typename关键字,也可以用class关键字,但是更推荐前者
Creating Instances of a Function Template
举个使用的例子↑
可以看到我们就像使用普通函数一样使用函数模板,T一般是不需要我们指定的,编译器会去进行推导,这种机制叫模板实参推导;编译器首先查找有没有double参数的larger版本函数,如果没有就会通过模板实例化生成一个double larger()
直到这里,实例化的过程就完成了;每个版本的函数只会生成一次,即使不同文件都包含类似的模板;再举一个例子:
Template Type Parameters
一般提到parameter,指的都是形参,是函数定义时的理想参数,而不是调用函数时传入的那个值
形参T可以实例化为很多类型,前面说过string不适合复制,所以我们可以使用const引用来写这个比较大小的函数:
事实上标准库里有一对max、min函数的实现逻辑就和这个例子类似:
Explicit Template Arguments
这里强调的是当调用实例的时候实参类型和模板矛盾的情况,举个例子:
这个larger我们用一个int和一个double传入函数再看,发现编译不通过,提示对于类型T有两个矛盾的推导类型,这说明虽然模板实现了一定程度上的类型参数化,但还是有死板的地方
为了解决这个问题,针对这里可以进行隐式转换的基本类型,模板的解决方法是这样的
我们需要在尖括号里显式指定需要的参数类型,这样的话编译器就不推导了,在调用时直接实例化一个larger<double>版本,并像普通函数一样对int进行隐式转换
所以我们如果显式指定int版本,那可预见的结果就是输出10和19,因为19.6是double会被转为int
Function Template Specialization
注意specialization和instantiation不是一个概念,前者指的是出现和模板类型不完全相符的类型参数,需要我们指定类型参数;后者指的是通过模板生成函数定义的过程,一般我们翻译两者为特化和实例化
如果我们传进去了地址,那么输出可能跟变量定义的顺序有关,因为此时的T会被推导为指针类型(存储的是两个变量的地址),而这两个局部变量的地址取决于编译器实现,所以这个输出是不可预料的
我们换了一下定义顺序结果就变了
a template specialization defines a behavior that is different from the standard template.
特化版本必须出现在标准版本之后实例化之前,否则无法编译
特化版本不需要尖括号里的形参T,因为我们指定了类型参数
不过书里说不建议特化函数模板,这种行为和typedef、new/delete一样是不推荐使用的语言特性
We recommend you never use function template specialization.
To customize a function template for specific types, you should overload the function template instead, either with a regular function or another function template.
We discuss these options in the next section.
Function overloads behave more intuitively in general than function template specializations.
Function Templates and Overloading
函数模板也是可以重载的,这样比模板特化更好,重载版本要比通用泛型版本更先被编译器选择
之前提到,同名函数只要参数列表不同就可以,虽然两个都可以调用,但是能不使用模板就不使用模板而是优先选择普通函数
甚至我们可以重载模板
注意这不是模板特化,这属于两个不同的模板,这个重载版本只有对指针类型参数才会实例化
重载也不是非要参数数量一致,重载个其他的版本也可以:
Function Templates with Multiple Parameters
之前提到的显式模板实参虽然可以解决不同类型参数推导矛盾的问题,但是更好的写法是加一个推导参数
对于a和b我们完全可以使用不同的类型T1,T2,这样参数可以被正确推导获取类型
但是有一个问题就是如果两个参数类型不同,返回值应该是什么类型,如这里的???位置应该是什么类型呢?
有一种解决办法是再加一个类型作为返回值,然后return的时候都转换为返回值类型:
这三行输出都指定了返回值类型是int,后两个则是对比较的两个数的类型进行了说明,这样似乎能解决问题
但是我们还是想做到不指定类型的编写方式,毕竟泛型要是指定类型也就不叫泛型了,一切类型都应该推导得来而不是写死在代码里
下一部分我们会讲到返回值推导,应该能做到自然的写法也能得出正确结果:
Return Type Deduction in Templates
针对上一小节三个问号那里的返回值应该怎么写,其实没有很好的解决方法,但是我们可以让编译器对这里的类型进行推导:
这样写的话虽然ab参数类型不同,但是还是可以返回较大值的类型的:
这里是输出正常结果的
decltype(auto)
考虑一个问题,如果我们这么写但是想要返回一个引用的时候,用auto该怎么做呢?因为auto的推导永远都是值类型,不会推导出指针或者引用
那么我们只能显式加上一个引用符号
但是这样又会出现第八章讲函数那里提到的问题,如果形参和实参不是一个类型,虽然是引用但还是会产生临时对象,我们的返回值还是一个局部对象的引用
这里程序不会输出正确结果,返回了一个错误代码,输出的编译信息是这样的
GDB单步调试发现这里的返回语句出现了段错误
针对这种情况,我们只能使用decltype(auto)这个语法来进行合适的类型推导
书上对这一点没有很详细的解释,我们可以看stackoverflow上有一个提问对于这一点的解释
c++ - What are some uses of decltype(auto)? - Stack Overflow
When to use decltype(auto) versus auto? - C++ Forum
后来发现这里的第二篇论坛解释更清楚,明确说到如果我们不知道函数要返回什么类型,或者是要根据参数类型来确定返回类型,那么我们不能写死为auto或者auto&,因为这两者的类型是明确的,要么是值要么就是引用,不灵活
所以这种模板里面一般使用decltype(auto),不是泛型函数就写死吧
(这里扯到了完美转发的技术,后面再说)
Default Values for Template Parameters
模板类型形参也是可以有默认值的
我们可以让他的默认返回值为double类型,当我们不指出返回类型的时候,也就是尖括号不用的时候,返回类型就是double
但是这里只说可以这么写没说这样写在larger函数里是好的做法,因为我们不是所有情况都想要double返回值;而且第二个重点是模板参数默认值和函数参数默认值不一样,可以有一定的灵活性
我们甚至还可以这么写:
用第一个参数来当第二个参数的默认值,只要T按顺序出现即可
不过由于标准库一般也是从右往左写,大家也都这么做了
Non-Type Template Parameters
假设我们要确认一个值是否在某区间里
对于C++20来说,模板形参可以是很多基本类型,数值、指针、引用、枚举等等,但是一般不写为class,限制很多我们不详细讲
由于这里非类型参数没有默认值,所以我们需要在尖括号里指出确定的区间两端分别是什么值
All template parameters, both type and non-type, need to be evaluated at compile time, while generating the concrete function instantiation from the template.
这里之所以把i定义为const,就是因为上面引用的这一条规则,模板参数必须是编译期可知的,否则无法生成函数实例
我们把const去掉就会发现无法编译:
如果我们不把这个范围边界写死为int,我们让编译器推导他的类型,使用起来会方便点
但是更好的做法是,把边界和传入的值设置为一个类型,即T:
虽然模板形参可以是非类型参数,但是一定要满足编译期可知的特点
Templates for Functions with Fixed-Size Array Arguments
这里的fixed-size指的是定长,即长度已经确定可知的情况,我们之前处理定长数组的方法是传一个引用
很明显如果我们不传引用的话就舍弃掉了数组的长度信息,但是有了模板我们就可以把长度也作为参数,此时编译器可以推导出这个长度:
其实标准库很多函数都是这么做的,让编译器去推导一个范围的长度,这样我们通过泛型就可以写出任意长度数组都可以用的函数了,这都归功于模板实参推导的机制
写一小段测试代码:
注意注释掉的第三个调用,充分地说明了数组名和指针本质不同
第四处我们传了一个列表,但是并不会生成参数是列表的新版本,而是接着用第二处调用生成的实例去推导参数
在C++20中我们也可以使用span来代替这种数组
如果类型是const T说明这种span不修改原序列,对const array也可以用
或者我们可以直接这样写:
这样定长不定长的序列我们都可以用,只不过不能修改序列内元素(不过求平均值确实也不用修改)
Abbreviated Function Templates
讲了这么多模板,其实模板写起来也挺冗余的,定义一个类型T就需要两个单词和一个括号显得有点繁琐
那么C++20对此做出的简化是
要注意,虽然不用template关键字,但这样写仍旧是函数模板
C++ template详解 | 普通人
这种写法叫做缩写函数模板,就是函数模板的语法糖,而且我们这里指的是参数列表中的auto而不是返回值的auto
每一个参数列表中的auto其实都会引入一个模板参数,语法糖就跟range-for类似,编译完之后还是有很多匿名的东西;当然我们也可以在尖括号里显式写出模板参数
这种写法涉及到编译问题,下一章会讲到
Limitations to Abbreviated Function Templates
语法糖香是香,但也是有限制的,像上面说到的,每一个参数列表中的auto都会引入一个新模板参数,所以如果我们想要两个参数类型相同的时候,就不能用这种缩写方式了
我们还是得采用传统写法,更别说函数内部使用参数类型了