【C++】提高代码的复用性—函数模板

发布于 2021-01-15 22:10

Simplicity is the shortest path to a solution.

学习郎推荐搜索
C++
LeetCode
数据结构
算法

本文讲述什么是函数模板(function template),并讨论怎样定义和使用函数模板。

01

函数模板定义

虽然下面的函数min()的算法很简单,但是,我们都希望实现一个实例:

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

有一种方法可替代这种“为每个min()实例都显式定义一个函数”的方法,这种方法很有吸引力,但是也很危险,那就是用预处理器的宏扩展设施。例如:

#define min(a, b) ((a) <(b)? (a) : (b))

虽然该定义对于简单的min()调用都能正常工作,如:

min(10, 20);min(10.0, 20.0);

但是,在复杂调用下,它的行为是不可预期的,这是因为它的机制并不像函数调用那样工作,只是简单地提供参数的替换。结果是,它的两个参数值都被计算两次:一次是在a和b的测试中,另一次是在宏的返回值被计算期间,例如:

#include <iostream> #define min(a,b) ((a) < (b) ? (a) : (b)) const int size = 10; int ia[size]; int main() {      int elem_cnt = 0;      int *p = &ia[0];      // 计数数组元素的个数     while ( min(p++,&ia[size]) != &ia[size] )      ++elem_cnt;      cout << "elem_cnt : " << elem_cnt      << "\texpecting: " << size << endl;      return 0; }

这个程序给出了计算整型数组ia的元素个数的一种明显绕弯的方法。min()的宏扩展在这种情况下会失败,因为应用在指针实参p上的后置递增操作随每次扩展而被应用了两次。即

((p++) < (&ia[size])? (p++) : (&ia[size]));    //min(p++, &ia[size])


函数模板提供了一种机制,通过它我们可以保留函数定义和函数调用的语义(在一个程序位置上封装了一段代码,确保在函数调用之前实参只被计算一次),而无需像宏方案那样绕过C++的强类型检查。

函数模板提供一种用来自动生成各种类型函数实例的算法。对于函数接口(参数和返回类型)中的全部或部分类型进行参数化(parameterize),而函数体保持不变。如用一个函数的实现在一组实例上保持不变,并且每个实例都处理一种惟一的数据类型,如函数min(),则该函数就是模板的最佳候选者。例如,下面是min()的函数模板定义:

template <class Type> Type min( Type a, Type b ) {     return a < b ? a : b; }int main() {      // ok: int min( int, int );      min( 10, 20 );      // ok: double min( double, double );      min( 10.0, 20.0 );     return 0;}


关键字template总是放在模板的定义与声明的最前面。关键字后面是用逗号分隔的模板参数表(template parameter list),它用尖括号(<>,一个小于号和一个大于号)括起来。该列表是模板参数表,不能为空。模板参数可以是一个模板类型参数(template type parameter),它代表了一种类型;也可以是一个模板非类型参数(template nontype parameter),它代表了一个常量表达式。

模板类型参数由关键字class或typename后加一个标识符构成。在函数的模板参数表中,这两个关键字的意义相同。它们表示后面的参数名代表一个潜在的内置或用户定义的类型。上例中,我们用Type来命名min()的模板参数,但实际上可以是任何名字。譬如:

template <class Glorp>     Glorp min( Glorp a, Glorp b ) {     return a < b ? a : b; }

当函数模板min()被实例化时,实际的内置或用户定义类型将替换模板的类型参数。类型int、dobule、char*、vector<int>或li<double>*都是有效的模板实参类型。

模板非类型参数由一个普通的参数声明构成。模板非类型参数表示该参数名代表了一个潜在的值,而该代表了模板定义中的一个常量。例如,size是一个模板非类型参数,它代表arr指向的数组的长度:

template <class Type, int size> Type min( Type (&arr) [size] );

当函数模板min()被实例化时,size的值会被一个编译时刻已知的常量值代替。

函数定义或声明跟 在模板参数表后。除了模板参数的类型指示符或常量值外,函数模板的定义看起来与非模板函数的定义相同。例:

template <class Type, int size> Type min( const Type (&r_array)[size] ) {      /* 找到数组中元素最小值的参数化函数 */     Type min_val = r_array[0];     for ( int i = 1; i < size; ++i )         if ( r_array[i] < min_val )             min_val = r_array[i];     return min_val; }

在例子中,Type表示min()的返回类型、参数r_array的类型,以及局部变量min_val的类型。size表示r_array引用的数组的长度。类型和值的替换过程被称为模板实例化(template instantiation)。


  • 如果在全局域中声明了与模板参数同名的对象、函数或类型,则该全局名将被隐藏。在下面的例子中,tmp的类型不是double,是模板参数Type:

typedef double Type; template <class Type> Type min( Type a, Type b ) {     // tmp 类型为模板参数 Type     // 不是全局 typedef     Type tmp = a < b ? a : b;     return tmp; }

  • 在函数模板定义中声明的对象或类型不能与模板参数同名:

template <class Type> Type min( Type a, Type b ) {     // 错误: 重新声明模板参数 Type     typedef double Type;     Type tmp = a < b ? a : b;     return tmp; }

  • 模板类型参数名可以被用来指定函数模板的返回位:

// ok: T1 表示 min() 的返回类型// T2 和 T3 表示参数类型template <class T1, class T2, class T3> T1 min( T2, T3 );

  • 模板参数名在同一模板参数表中只能被使用一次。例如,下面代码就有编译错误:

// 错误: 模板参数名 Type 的非法重复使用template <class Type, class Type> Type min( Type, Type );

但是,模板参数名可以在多个函数模板声明或定义之前被重复使用:

// ok: 名字 Type 在不同模板之间重复使用template <class Type> Type min( Type, Type ); template <class Type> Type max( Type, Type );

  • 一个模板定义和多个声明所使用的模板参数名无需相同。例如,下列三个min()的声明都指向同一个函数模板:

// 三个 min() 的声明都指向同一个函数模板// 模板的前向声明template <class T> T min( T, T ); template <class U> U min( U, U ); // 模板的真正定义template <class Type> Type min( Type a, Type b ) { /* ... */ }

  • 模板参数在函数参数表中可以出现的次数没有限制。在下面的例子中,Type用来表示两个不同函数参数的类型:

#include <vector> // ok: 在模板函数的参数表中多次使用 Type template <class Type> Type sum( const vector<Type> &, Type );

  • 如果一个函数模板有一个以上的模板类型参数,则每个模板类型参数前面都必须有关键字class或typename。

// ok: 关键字 typename 和 class 可以混用template <typename T, class U>  T minus( T*, U ); // 错误: 必须是 <typename T, class U> 或 <typename T, typename U> template <typename T, U> T sum( T*, U );


02

函数模板实例化

函数模板指定了怎样根据一组或更多实际类型或值构造独立的函数。这个构造过程被称为模板实例化(template instantiation)。这个过程是隐式发生的,它可以被看作是函数模板调用或取函数模板的地址的副作用。例如,在下面的例子中,min()被实例化两次:一次是针对5个int的数组类型,另一次是针对6个dobule的数组类型。

// 函数模板 min() 的定义// 有一个类型参数 Type 和一个非类型参数 sizetemplate <typename Type, int size>  Type min( Type (&r_array)[size] ) {     Type min_val = r_array[0];     for ( int i = 1; i < size; ++i )         if ( r_array[i] < min_val )             min_val = r_array[i];     return min_val; } // size 没有指定——ok // size = 初始化表中的值的个数int ia[] = { 10, 7, 14, 3, 25 }; double da[6] = { 10.2, 7.1, 14.5, 3.2, 25.0, 16.8 }; #include <iostream> int main() {     // 为 5 个 int 的数组实例化 min()     // Type => int, size => 5     int i = min( ia );     if ( i != 3 )         cout << "??oops: integer min() failed\n";     else cout << "!!ok: integer min() worked\n";     // 为 6 个 double 的数组实例化 min()     // Type => double, size => 6     double d = min( da );     if ( d != 3.2 )         cout << "??oops: double min() failed\n";     else cout << "!!ok: double min() worked\n";     return 0; }

调用

int i = min(ia);

被实例化为下面的min()的整型实例,这里Type被int、size被5取代:

int min( int (&r_array)[5] ) {     int min_val = r_array[0];     for ( int ix = 1; ix < 5; ++ix )         if ( r_array[ix] < min_val )             min_val = r_array[ix];     return min_val; }

类似地,调用

double d = min(da);

也实例化了 min()的实例,这里 Type double size 被 6 取代。

类型参数Type和非类型参数size都被用作函数参数。为了判断用作模板实参的实际类型和值,编译器需要检查函数调用中提供的函数实参的类型。在我们的例子中,ia的类型(即5个int的数组)和da的类型(即6个double的数组)被用作决定每个实例的模板实参。用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演(template argument deduction)。


  • 函数模板在它被调用或取其地址时被实例化。在下面例子中,指针pf被函数模板实例的地址初始化。编译器通过检查pf指向的函数参数类型来决定模板实例的实参。

template <typename Type, int size> Type min( Type (&p_array)[size] ) { /* ... */ } // pf 指向 int min( int (&)[10] ) int (*pf)(int (&)[10]) = &min;

pf的类型是指向函数的指针,该函数有一个类型为int(&)[10]的参数。当min()被实例化时,该参数的类型决定了Type的模板实参的类型和size的模板实参的值,指针pf指向这个模板实例。

  • 在取函数模板实例的地址时,必须能够通过上下文环境为一个模板实参决定一个惟一的类型或值。如果不能决定出这个惟一的类型或值,就会产生编译时刻错误。例如:

template <typename Type, int size>  Type min( Type (&r_array)[size] ) { /* ... */ } typedef int (&rai)[10]; typedef double (&rad)[20]; void func( int (*)(rai) ); void func( double (*)(rad) ); int main() {     // 错误: 哪一个 min() 的实例?     func( &min ); }

因为函数func()被重载了,所以编译器不可能通过查看func()的参数类型,来为模板参数Type决定惟一的类型,以及为size的模板实参决定一个惟一值。调用 func()无法实例化下面的任何一个函数:

min( int (*)(int(&)[10]) ) min( double (*)(double(&)[20]) )

因为不可能为func()指出一个惟一的实参的实例,所以在该上下文环境中取函数模板实例的地址会引起编译时刻错误。

如果我们用一个强制类型转换显式地指出实参的类型则可以消除编译时刻错误:

int main() {  // ok: 强制转换指定实参类型    func( static_cast< double(*)(rad) >(&min) ); }


03

模板实参推演

当函数模板被调用时,对函数实参类型的检查决定了模板实参的类型和值、这个过程被称为模板实参推演(template argument deduction)。

函数模板min()的函数参数是一个引用,它指向了一个Type类型的数组:

template <class Type, int size> Type min( Type (&r_array)[size] ) { /* ... */ }

为了匹配函数参数,函数实参必须也是一个表示数组类型的左值。下面的调用是个错误,因为pval是int*类型而不是int数组类型的左值。

void f( int pval[9] ) {     // 错误: Type (&)[] != int*     int jval = min( pval ); }

在模板实参推演期间决定模板实参的类型时,编译器不考虑函数模板实例的返回类型。例如,对于如下的min()调用:

double da[8] = { 10.3, 7.2, 14.0, 3.8, 25.7, 6.4, 5.5, 16.8 }; int i1 = min( da );

min()的实例有一个参数,它是一个指向8个double的数组的指针。该实例返回的值的类型是double型。该返回值先被转换成int型,然后再用来初始化i1。即使调用min()的结果被用来初始化一个int型的对象,也不会影响模板实参的推演过程。


要想成功地进行模板实参推演,函数实参的类型不一定要严格匹配相应函数参数的类型。下列三种类型转换是允许的:

  1. 左值转换

左值转换包括从左值到右值的转换、从数组到指针的转换或从函数到指针的转换

template <class Type> // 第一个参数是 Type* Type min2( Type* array, int size ) {     Type min_val = array[0];     for ( int i = 1; i < size; ++i )         if ( array[i] < min_val )             min_val = array[i];     return min_val; }

我们可以用4个int的数组来作为第一个实参调用min2(),如下:

int ai[4] = { 12, 8, 73, 45 }; int main() {     int size = sizeof (ai) / sizeof (ai[0]);     // ok: 从数组到指针的转换    min2( ai, size ); }

函数实例ai的类型是4个int的数组,虽然这与相应的函数参数类型Type*并不严格匹配。但是因为允许从数组到指针的转换,所以实参ai在模板实参Type被推演之前被转换成int*型。Type的模板实参接着被推演为int,最终被实例化的函数模板是min2(int*, int)。

  1. 限定转换

限定修饰转换把const或volatile限定修饰符加到指针上。如下:

template <class Type>  // 第一个参数是 const Type*  Type min3( const Type* array, int size ) {  //... }我们可以用int*型的第一个参数调用min3(),如下:int *pi = &ai; // ok: 到 const int* 的限定修饰转换int i = min3( pi, 4 );

函数实参pi的类型是int指针,虽然与相应的函数参数类型const Type*并不完全匹配。但是因为允许限定修饰转换,所以函数实参在模板实参被推演之前,就先被转换const Type*型了。然后Type的模板实参被推演为int,被实例化的函数模板是min3(const int*, int)。

  1. 到一个基类的转换

如果函数参数的类型是一类模板,且如果实参是一个类,它有一个从被指定为函数参数的类模板实例化而来的基类,则模板实参的推演就可以进行。例:

template <class Type> class Array { /* ... */ }; template <class Type> Type min4( Array<Type>& array ) {     Type min_val = array[0];     for ( int i = 1; i < array.size(); ++i )         if ( array[i] < min_val )             min_val = array[i];     return min_val; }

我们可以用类型ArrayRC<int>的第一个实参调用min4(),如下:

template <class Type> class ArrayRC : public Array<Type> { /* ... */ }; int main() {    ArrayRC<int> ia_rc( ia, sizeof(ia)/sizeof(int) );    min4( ia_rc ); }

函数参数ia_rc的类型是ArrayRC<int>,它与相应的函数参数类型Array<Type>&并不完全匹配。因为类ArrayRC<int>有一个Array<int>的基类,而Array<int>是一个从被指定为函数参数的类模板实例化而来的类,并且派生类类型的函数实参还可以被用来推演一个模板实参,所以函数实参ArrayRC<int>在模板实参被推演之前首先被转换成Array<int>型,然后Type的模板实参再被推演为int,被实例化的函数模板是min4(Array<int>&)。


所以模板实参推演的通用算法如下:

  1. 依次检查每个函数实参 ,以确定在每个函数参数的类型中出现的模板参数。

  2. 如果找到模板参数,则通过检查函数实参的类型 推演出相应的模板实参。

  3. 函数参数类型和函数实参类型不必完全匹配 。下列类型转换可以被应用在函数实参上,以便将其转换成相应的函数参数的类型:

    • 左值转换。

    • 限定修饰转换。

    • 从派生类到基类类型的转换,假定函数参数具有形式T<args>、T<args>&或T<args>*,则这里的参数表args至少含有一个模板参数。

  1. 如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同。

END

本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材