C++ Template 函数模板

函数模板

函数模板就是参数化函数,它本身就代表一个函数族。

什么是函数模板
函数模板为许多类型提供了不同的行为。同时也是解放程序员双手的利器,想一下如果你需要为一个函数编写一组重载函数,而且它们的行为一致,那么你需要反复复制粘贴大量代码,但是函数模板就可以将这些繁杂的工作留到幕后,你只需要定义一个函数模板和偏特化或为某些特殊类型进行重载(如果有的话)即可。

定义模板
以下是一个函数:

int ::max(int a,int b){
return b < a ? a : b;
}

如果想要将这个函数泛化,也就是将它编写成一个函数模板,可以这样做:

template<typename Ty_>
Ty_ ::max(Ty_ a,Ty_ b){
return b < a ? a : b;
}

现在该模板就代表了一组函数,其中 template< typename tmpArgs > 用来声明模板参数,模板参数列表是 < typename tmpArgs> ,关键字 typename 用来引入类型参数,后跟代表类型的占位符 tmpArgs,可以用任何类型实例化该模板,只要该类型提供了模板函数体内对类型参数使用的操作就没问题,否则会编译失败,提示没有为该类型定义某某运算符,在这里是’<‘。顺便提一下,可以使用class关键字替代typename关键字,但是不提倡这样做,因为classC++11之前用来为模板引入参数的关键字,那个时候还没有typename关键字,而且class关键字会让人有歧义认为只有类类型才能作为模板实参,而typename语义则更加明确。

使用模板

#include <iostream>
#include <string>
using namespace std;

template<typename Ty_>
Ty_ ::max(Ty_ a,Ty_ b){
return b < a ? a : b;
}

int main(){
::max(1,2);
::max(std::string{"hell"},std::string{"no"});
}

在上面的调用中,::max()是该函数的全限定名,如果没有使用全限定名,那么会在一些情况下与标准库定义的std::max引发冲突,例如:

namespace std{
class My_cls{};
}
...
max(My_cls{},My_cls{});//ERROR!是std::max还是::max?

不要误认为编译器会将该模板生成为一个可以处理任何类型的单个函数,编译器会在使用模板的时候查看模板的定义并生成该类型的版本,生成的类型与程序员手写的函数一模一样,没有任何性能上以及其他方面的区别。
用具体类型替代模板参数的过程称为模板实例化。它会生成该模板参数的实例。请注意使用模板就会触发模板实例化,不用单独请求实例化。

二阶段翻译

第一阶段:由于在定义时没有被实例化,模板代码本身会忽略模板参数正确性的检查。例如:

  • 常规的语法错误检查
  • 使用了不依赖于模板参数的未声名名字
  • 检查不依赖于模板参数的静态断言

第二阶段:在实例化时,再次检查模板代码确保所有代码有效无误,也就是依赖于模板实参的部分都经过双重检查。

template <typename T>
void foo(T t)
{
undeclared();        // 第一阶段编译时错误 
undeclared(t);        // 第二阶段编译时错误  
static_assert ( false,"error" );		// 第一阶段失败
static_assert ( sizeof (T) > 10,"T too small");			//第二阶段失败
}

二阶段翻译导致了模板处理的一个重要问题:当函数模板以触发实例化的方式使用时(实例化点),编译器需要查看模板的完整定义,所以函数模板和普通函数的编译和链接的区别是完全不一样的,所以模板的声明和定义不能分离编译。

模板参数推导
当我们用具体类型实例化上面的::max()函数模板时,Ty_被推导为具体类型,例如如果传递的两个实参的类型是int,那么Ty_就被推导为int,但是,Ty_可能只是类型的一部分。例如:

template <typename Ty_>
Ty_ max (Ty_ const & a, Ty_ const & b){
    return b < a ? a:b;
}

如果我们传递参数的类型为int,那么Ty_就是int,因为int匹配const int&。
但是隐式类型转换在模板实参推断中受到限制:
当模板函数参数是传引用调用时,即使是微不足道的转换也不适用,使用相同模板参数声明的两个形参Ty_的类型必须完全一致。
当模板函数参数是传值调用时,只支持衰减转换:忽略cv限定,原始数组转或函数换为相应的指针类型,引用转换为被引用类型,最后,衰减类型必须匹配。

template<typename Ty_>
void foo(Ty_ a, Ty_ b) {

}

template<typename Ty_>
void bar(Ty_& a, Ty_& b) {

}

int main() {
	const char s1[]{ "abc" };
	char s2[]{ 'a','b','c','\0' };
	int a{};
	const int b{};
	foo("fsdada", "fweds");		//ok
	foo(a, b);					//ok
	bar(a, b);					
	bar(s1, s2);
	foo("hello",4);				//注:参数1被推导为数组,非指针
}

有三种方式处理这些问题:

  • 显式类型转换
bar(const_cast<const int>(a),b);
  • 明确指定模板参数
::bar<const int>(a,b);
  • 指定参数可以有不同的类型
templatr<typename Ty_1,typename Ty_2>
...
  1. 默认参数的类型推导:
    类型推导不适用于默认参数
template < typename Ty_>
void f(Ty_ = "" );
...
::f(1);        // OK:推断 T 为 int ,因此它调用 ::f<int>(1)
::f();         // ERROR:无法推断出 T

可以为模板参数声明默认参数:

template<typename Ty_ = std::string>
f();		//OK
  1. 多模板参数
    在一个函数模板中有两种参数,分别是:
  • 模板参数,声明在尖括号中的参数列表
template<typename Ty_>
  • 调用参数,声明在函数名的圆括号参数列表中
T ::max(T a, T b);

可以根据需要拥有任意数量的模板参数。例如,您可以 为两种可能不同类型的调用参数定义::max()模板:

template<typename Ty_1typename Ty_2>
Ty_1 max (Ty_1 a, Ty_2 b);

该函数模板的返回类型由第一个模板参数决定,那么有什么办法能够在某种条件下返回模板参数列表中的一个呢?
有三种方式解决:
• 引入第三个模板参数作为返回类型。

template <typename Ty_1typename Ty_2typename Ty_3>
Ty_3 max (Ty_1 a, Ty_2 b);

但是,模板参数推导不考虑返回类型, 并且 RT 不出现在函数调用参数的类型中。因此, 不能推断出返回类型。因此必须明确指定模板参数列表:

::max< int , double , double >(4, 7.2);

但是,可以将返回的模板参数的位置放在第一个,在调用的时候显式指定第一个参数,剩余参数由模板自行推导。

template <typename Ty_1typename Ty_2typename Ty_3>
Ty_1 max (Ty_3 a, Ty_2 b);
...
::max<int>(4, 7.2);

• 让编译器自己推断出返回类型。
如果返回类型依赖于模板参数,推导返回类型的最简单和最好的方法是让编译器自行推断。从 C++14 开始,通过简单地不声明任何返回类型来实现(将返回类型声明为 auto ):

template<typename Ty_1typename Ty_2>
auto (Ty_1 a, Ty_2 b);

没有使用尾随返回类型表明返回类型由return语句推导出,从函数体中推断返回类型必须是可能的。因此,代码必须可用并且多个返回语句必须匹配。
在 C++14 之前,只能通过或多或少地将函数的实现作为其声明的一部分来让编译器确定返回类型。在 C++11 中,我们可以受益于尾随返回类型语法允许我们使用调用参数这一事实。也就是说,我们可以声明返回类型是从:?运算符推导

template <typename Ty_1, typename Ty_2>
auto max (Ty_1 a, Ty_2 b)->decltype (b<a?a:b) {
return b < a ? a : b;
}

编译器将使用:?运算符的规则推导出a和b的公共类型,编译器并不会计算该表达式,也就是意味着decltype(b<a?a:b)可以改写为true?a:b
因为decltype()推导出的类型不会进行类型扣除,在一些情况下会返回我们不需要的类型,可以通过std::decay_t<decltype()>来实现。
• 将返回类型声明为两种参数类型的“通用类型”。
C++标准库提供了一个模板来指定”更通用的类型“:std::common_type_t<>,它会为模板所有参数推导一个公共类型,但是是衰减的:

template <typename Ty_1, typename Ty_2>
std::common_type_<Ty1,Ty_2>max(Ty_1 a,Ty_2 b){
return b < a ? a: b;
}

默认模板参数
您还可以为模板参数定义默认值。这些值称为默认模板参数,可用于任何类型的模板。 他们甚至可能参考以前的模板参数。例如,如果你想将定义返回类型的方法与具有多个参数类型的能力结合起来,你可以为返回类型引入一个模板参数,该模板参数具有两个参数的公共类型作为默认值。同样,我们有多种选择:

template <typename Ty_1typename Ty_2typename Ty_3 = std::decay_t <decltype (true ? T1() : T2())>>
Ty_1 max (Ty_3 a, Ty_2 b);

我们也可以使用 std::common_type_t<> 来指定返回类型的默认值:

template <typename Ty_1typename Ty_2typename Ty_3 = std::common_type_t<T1,T2>>
Ty_1 max (Ty_3 a, Ty_2 b);

最好和最简单的解决方案是让编译器推断返回类型。

重载函数模板
与普通函数一样,函数模板可以被重载。也就是说,您可以拥有具有相同函数名
称的不同函数定义,以便在函数调用中使用该名称时,C++ 编译器必须决定调用
各种候选者中的哪一个。这个决定的规则可能会变得相当复杂,即使没有模板。

int max (int a,
int b)
{
return b < a ? a : b;
}
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
int main()
{
::max(7, 42); // calls the nontemplate for two ints
::max(7.0, 42.0); // calls max<double> (by argument deduction)
::max(’a’, ’b’); //calls max<char> (by argument deduction)
::max<>(7, 42); // calls max<int> (by argument deduction)
::max<double>(7, 42); // calls max<double> (no argument deduction)
::max(’a’, 42.7); //calls the nontemplate for two ints
}

示,非模板函数可以与具有相同名称且可以使用相同类型实例化的函数模板共存。在所有其他因素相同的情况下,重载解析过程更喜欢非模板而不是从模板生成的非模板。

也可以明确指定一个空的模板参数列表。此语法表明只有模板可以解析调用,但所有模板参数都应从调用参数中推导出来:

::max<>(1,2);

一个有趣的例子是重载最大模板以便能够明确指定返回类型:

template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

auto a = ::max(4, 7.2);                     // uses first template
auto b = ::max<long double>(7.2, 4);        // uses second template

auto c = ::max<int>(4, 7.2);                // ERROR: both function templates match

两个模板都匹配,这导致重载解析过程出现歧义。因此,在重载函数模板时,您应该确保只有其中一个匹配任何调用。

按值传递还是按引用传递
为什么我们通常将函数声明为按值传递参数而不是使用引用。通常,对于廉价简单类型以外的类型(例如基本类型或 std::string_view ),建议通过引用传递,因为不会创建不必要的副本。然而,出于几个原因,通常按值传递通常更好:
• 语法简单。
• 编译器优化得更好。
• 移动语义通常使复制变得廉价。
• 有时根本没有复制或移动。
此外,对于模板,特定方面会发挥作用:
• 模板可能同时用于简单类型和复杂类型,因此为复杂类型选择方法可能对简单类型适得其反。
• 作为调用者,您通常仍然可以决定通过引用传递参数,使用 std::ref() 和 std::cref() 。
• 尽管传递字符串文字或原始数组总是会成为一个问题,但通过引用传递它们通常被认为是个更大的问题。

函数模板内联
一般来说,函数模板不必用 inline . 与普通的非内联函数不同,我们可以在头文件中定义非内联函数模板,并将这个头文件包含在多个翻译单元中。
此规则的唯一例外是特定类型的模板的完全特化,因此生成的代码不再是通用的(所有模板参数都已定义)。

a.h:
template<typename Ty_>
void f(){}				//正常编译链接
		
template<>
inline void f<int>(){}	//正常编译链接
			
template<>
void f<char>(){}		//重定义错误
a.c:
#include "a.h"
b.c:
#include "a.h"

inline有两种语义:
• 可以在程序中出现多次定义;
• 扩展内联,生成高效代码。

总结
• 函数模板为不同的模板参数定义了一系列函数。
• 当您根据模板参数将实参传递给函数参数时,函数模板会推导出要为相应参数类
型实例化的模板参数。
• 您可以显式限定前导模板参数。
• 您可以为模板参数定义默认参数。这些可能引用以前的模板参数,后面跟着没有
默认参数的参数。
• 您可以重载函数模板。
• 当用其他函数模板重载函数模板时,您应该确保只有其中一个与任何调用匹配。
• 重载函数模板时,将更改限制为明确指定模板参数。
• 确保编译器在您调用它们之前看到函数模板的所有重载版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值