模板函数

引入

现在要实现一个比较函数 cmp,要求实现 int,double,string(按字典序)的大小比较。

一般来讲,常用的做法是写 4 个重载函数,分别对应 4 种不同的实参类型。

不想这么麻烦?似乎可以使用无类型指针强行对这四种类型做适配。

但是无类型指针每次调用之前必须显式地告诉它数据类型,但是这个数据类型却并不方便一起带入函数。

那么有没有一种省时省力省心的做法呢?

答案就是 template(模板)。

声明模板类型

template < typename T1, typename T2, ..., typename Tn >
//or 
template < class T1, class T2, ..., class Tn >

一般情况下,我们推荐第一种写法(因为 class 已经用于定义类了,为了区分开它们,推荐使用 typename)。

其中 \(T_1, T_2, ... T_n\) 分别代表一种未定的数据类型,我们会在下面再说如何(自动)初始化它们为实际的类型。

声明模板参数

模板参数和模板类型不同,它们是一个确定类型的数字

template < int T1, unsigned T2, size_t T3, ... >

其中 \(T_1, T_2, ... T_n\) 分别代表一个确定类型的常数,我们会在下面再说如何(自动)初始化它们为实际的数。

注意:并不是所有的类型都可以做模板参数的类型。(比如 double 就不可以)

函数模板

#include <iostream>
#include <cstring>

using namespace std;

bool cmp(const int& a, const int& b)
{
  return a < b;
}
bool cmp(const double& a, const double& b)
{
  return a < b;
}
bool cmp(const string& a, const string& b)
{
  return a < b;
}

template < typename T >
bool cmpT(const T& a, const T& b)
{
  return a < b;
}

int main()
{
  int a = 1, b = 2;
  double c = 1.1, d = 2.2;
  string e = "dad", f = "mom";

  cout << cmp(a, b) << cmp(d, c) << cmp(e, f) << endl;
  cout << cmpT < int > (a, b) << cmpT < int > (d, c) << cmpT < int > (e, f) << endl;
  
  return 0;
}

在这段代码中,有三个重载的 cmp 函数,另有一个模板函数 cmpT,两个 cout 输出的结果是一样的 101。

cmpT 用更短的代码完成了三个重载函数的工作。

cmpT 同时也更灵活——假设我现在要把 return 语句中的 < 改成 >,那么 cmpT 只用修改一个地方,而每一个重载函数都需要修改。

我们现在来看看 cmpT 的工作原理吧:

函数模板工作原理

template < typename T > 一句声明下面一行的函数(注意仅包括下一行代码)作为模板使用,这个模板是一个蓝图,可以用来制造函数

T 的作用类似于一个未知数,它表示“未定的数据类型”,我们称为模板类型

一旦 T 的类型确定,编译器就可以根据模板里的代码生成一个函数,具体做法是把 T 替换为给定的数据类型(T -> X; T* -> X*; T& -> X&... 其中 X 是一种具体类型)。

比如给定的数据类型是 int,那么上面的 cmpT 模板就会生成一个这样的函数:

bool cmpT(const int& a, const int& b)
{
  return a < b;
}

这个生成的过程称为模板实例化

在编译时,编译器根据调用该函数时的参数情况来实例化一个具体的函数。根据不同的参数类型实例化出来的函数互为重载函数(函数名相同,参数表不同)。

函数模板使用规范

指定类型/值

指定模板类型(的类型)的方法是在函数名和参数之间加上以尖括号括起来,逗号分隔开的数据类型。

指定模板参数(的值)的方法类似。

譬如:

template < typename T1, typename T2, size_t T3 >
T1 Add(const T1& a, const T2& b)
{
  T1 sum = a + b + T3;
  return sum;
}
cout << Add < int, double, 40 > (a, c) << ' ' << Add < double, double, 40 > (b, d) << endl;

/*
call of Add < int, double, 40 > (a, c)
build function:
int Add(const int& a, const double& b)
{
  int sum = a + b + 40;
  return sum;
}
*/

/*
call of Add < double, double, 40 > (b, d)
build function:
double Add(const double& a, const double& b)
{
  double sum = a + b + 40;
  return sum;
}
*/

其中,< int, double, 40 > 就把 T1 指定为 int,T2 指定为 double,T3 指定为 40;< double, double, 40 > 就把 T1 指定为 double,T2 指定为 double,T3 指定为 40。(T3 在它的作用范围内是一个常数,用来初始化 T3 的表达式也必须是一个常量表达式

另外,编译器可以根据带入实参的类型推断模板类的类型应该是什么。

template < typename T >
bool cmpT(const T& a, const T& b)
{
  return a < b;
}

对于这段代码,cmpT < int > (a, b) 等价于 cmpT(a, b);(实参 a, b 均为 int 类型)。

特别的,如果仅根据实参无法确定所有模板类型的实际类型以及所有模板参数的值,需要使用尖括号的方法显式按顺序注明未确定的模板类型实际类型(或模板参数的实际值)。

template < typename T1, typename T2, typename T3, size_t T4 >
T3 Add3(const T1& a, const T2& b)
{
  T3 sum = a + b + T4;
  return sum;
}

Add3(a, c)//Error! 无法确定 T3 的类型和 T4 的值
Add3 < int, double, long long, 40 > (a, c);//OK! 指定 T3 为 long long 类型,T4 = 40

template < typename T3, size_t T4, typename T1, typename T2 >
T3 Add3(const T1& a, const T2& b)
{
  T3 sum = a + b;
  return sum;
}
Add3(a, c)//Error! 无法确定 T3 的类型和 T4 的值
Add3 < long long, 40 > (a, c);//OK! 指定 T3 为 long long 类型,T4 = 40,T1,T2 类型自动检测

template < size_t T >
void f(char (&a)[T])
{
//do sth...
}// T 被自动初始化为作为实参的数组的长度

指定类型必须对模板函数内的操作有定义

如果 T 类型代表了自定义类型 Complex(复数),而这个类没有重载小于号(实际上,虚数不能比较大小),那么下面的模板就没法正常实例化:

template < typename T >
bool cmpT(const T& a, const T& b)
{
  return a < b;
}

因为在把 T 换成 Complex 的时候,找不到重载的小于号运算符来执行 a < b;

因此,在编写函数模板时,要注意可能的参数类型对模板内的操作均有定义才行。

模板类型无法进行隐式类型转换

int fun1(int x, int y)
{
  return x + y;
}

template < typename T >
T fun2(T x, T y)
{
  return x + y;
}

int main()
{
  int a = 1;
  double b = 2.2;
  fun1(a, b);// OK! result = 3
  fun2(a, b)// Error!
}

在 fun1 中,发生了 double 到 int 的隐式类型转换,在 fun2 中无法发生这样的转换。

因为没有给出尖括号来初始化 T,编译器发现 T 对应的实参一个是 int 类型,一个是 double 类型,它无法确定到底 T 应该是 int 还是 double,因此也就无法完成类型转换。

如果显示指定 T 的类型 fun2 < int > (a, b); 则可以避免这样的错误,编译器会把 double 隐式转化为 int。

重载函数模板

类似于普通函数的重载,模板与模板之间、模板与普通函数之间也可以发生重载。

在调用的时候,编译器会根据参数的类型与数量选择最适合的实例化函数/普通函数来调用。

注意:如果模板实例化得来的函数和普通函数同样适合,优先调用普通函数。

来看下面这段代码和他的运行结果

#include <iostream>
#include <cstring>

using namespace std;

template < typename T >
void fun1(const T& a, const T& b)
{
  cout << "template fun1 called\n";
}
void fun1(const int& a, const int& b)
{
  cout << "normal fun1 called\n";
}

template < typename T >
void fun2(const T& a, const T& b)
{
  cout << "template fun2 called\n";
}
void fun2(const int& a, const int& b)
{
  cout << "normal fun2 called\n";
}

template < typename T >
void fun3(T* const a, T* const b)
{
  cout << "fun3 with T* called\n";
}
template < typename T >
void fun3(const T& a, const T& b)
{
  cout << "fun3 with T& called\n";
}

int main()
{
  int a = 1, b = 2;
  double c = 1.1, d = 2.2;
  fun1(a, b);
  //normal fun1 called
  //模板实例化函数和普通函数同样适合,优先调用普通函数
  fun1 < > (a, b);
  //template fun1 called
  //带 < > 是指定使用模板实例化函数的方法,因为编译器可以通过 a, b 推断出 T 的实际类型,所以尖括号可以为空
  fun1 < int > (a, b);
  //template fun1 called
  //带 < > 并且显式注明了 T 的类型为 int,和上一个等价
  //如果带 < > 并且显式注明了别的类型,编译器会尝试隐式转换 int 为那个类型
  fun2(a, b);
  //normal fun2 called
  //同上优先普通函数
  fun2(a, 'a');
  //normal fun2 called
  //模板实例化函数无法隐式转换,只能调用普通函数
  fun2(c, d);
  //template fun2 called
  //模板实例化函数参数为两个 double,更为适合
  fun3(a, b);
  //fun3 with &T called
  //int 类型实参,用 T& 的实例化函数更合适
  fun3(&a, &b);
  //fun3 with T* called
  //int* const 类型实参,用 T* const 的实例化函数更合适
  return 0;
}

函数模板的默认值

模板类型的默认值

和普通函数一样,模板类型也可以有默认值,如下:

template < typename X = int, typename Y >
X f(Y a, Y b)
{
  return a + b;
}

int main()
{
  double x = 1.2, y = 3.5;
  cout << f(x, y) << '\n';//result = 4
  cout << f < double > (x, y) << '\n';//result = 4.7
  return 0;
}

我们默认 X 代表的类型是 int,输出 4;当显式指定 X 的类型是 double,输出 4.7。

注意:默认值会被参数带来的类型覆盖掉(这不是常识吗,但是类型默认值比较新奇,所以提一下)

template < typename X = int, typename Y >
X f(X a, Y b)
{
  return a + b;
}
int main()
{
  double x = 1.2, y = 3.5;
  cout << f(x, y) << '\n';//result = 4.7
  return 0;
}

模板参数的默认值

template < typename X = int, size_t T = 40 >
X* f()
{
  X* p = new X [T];
  return p;
  //默认申请 40 个 int 类型的空间,返回空间首地址指针
}

int main()
{
  int* p1 = f();
  //申请 40 个 int 类型的空间交给 p1
  double* p2 = f < double, 20 > f();
  //申请 20 个 int 类型的空间交给 p2
}

同样的,模板参数的默认值一旦在调用时给出,就采用调用时给出的值,这和普通函数的默认值的操作是一样的。

template < typename X >
X f(X a = 40)
{
  return a;
}

传统的在参数表里的默认值也是被允许的,但是这样做的前提是必须存在一种转换方法把 40(int 类型)转换为 X 类型。

函数模板的特化

在调用函数模板时,我们可能需求对某种特定的参数组合做特殊的处理。

template < typename T1, typename T2 >
void f(T1 a, T2 b)
{
  cout << "template f called\n";
}

int main()
{
  int a = 1, b = 2;
  double c = 1.1, d = 2.2;
  f(a, b);//(int, int)
  f(a, c);//(int, double)
  f(c, a);//(double, int)
  f(c, d);//(double, double)
}

现在希望对参数表为 (double, int) 的模板做重载。

template < >
void f < double, int > (double a, int b)
//指定 T1 为 double,T2 为 int,实参的类型也显式注明
{
  cout << "specialized template f called\n";
}

在模板声明中不声明任何模板类型,而直接在下面显式注明。

特化模板相当于对模板 void f(T1 a, T2 b) 做一个特殊约定:当参数表为 (double, int) 时,调用特化模板。该模板函数的返回值必须与上面做约定的模板相同,参数表必须是后者的一种特殊情况。

参数表的类型由前面的尖括号依次给出 T1,T2 的类型,并且特化模板函数的实参类型也必须显式注明,而不允许使用 T1,T2 来指代(尽管它们已经被指定了)。

这样,f(c, a); 命令就会优先调用这个函数了。

一般情况下,不建议使用特化函数,而直接使用重载的普通函数更为方便。

posted @ 2023-12-06 09:09  ZTer  阅读(12)  评论(0编辑  收藏  举报