“ 千里之行始于足下”
01 前言
我们知道C++作为一种强类型语言,有强类型语言可靠安全的优势。但有时实现一些相对简单的函数似乎是个障碍,比如我们要实现swap、min、max这些函数的时候,强类型语言要求我们为所有希望比较的类型都实现一个实例,那似乎是很繁琐无趣的,当然我们可以通过定义宏来实现,比如:#define min(a,b) ((a) < (b) ? (a) : (b)),但是在复杂调用下,它的行为是不可预期的,这是因为宏机制并不像函数调用那样,他只是简单的参数替换。我们来看C++Primer中一个经典的例子:
所以很多实际项目的编码规范中是禁止使用这种宏函数的。此时函数模板就可以派上用场了,函数模板提供了一种机制,它可以让我们保留函数定义和函数调用的语义,而无需像宏方案那样绕过 C++的强类型检查。函数模板提供一个用来自动生成各种类型函数实例的算法。我们可以用函数模板,对函数接口中的返回值/参数进行全部或部分类型的参数化,但函数体保持不变。如果一个函数的实现在一组实例上都保持不变,并且每个实例都处理一种惟一的数据类型,则该函数就是模板的最佳候选者。函数模板摆脱了类型的限制,提供了通用的处理,极大提升了代码的重用性。
02 函数模板定义
函数模板不是一个实在的函数,函数模板是用来定义一族函数的,在函数模板实例化之前,编译器不能为其生成可执行代码,当它具体执行时,将根据传递的实际参数决定其功能。
函数模板定义格式:
template function-declaration
细化如图所示:
从上图看,函数模板在形式上分为两部分:模板(template)和函数(function-declaration)。
模板(template):
这部分我们需要对模板的参数列表(parameter-list)做下说明,parameter-list是由英文逗号(,)分隔的列表,每项可以是下列之一:
序号 | 名称 | 说明 |
---|---|---|
1 | 非类型形参 | 非类型形参在模板的内部是常量; 非类型模板的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针是正确的; 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果; 主要形式有如下三种: int Nint N = 1: 带默认值,该值必须是一个常量或常量表达式 int ...N: (可变参数模板) |
2 | 类型形参 | swap值用的形式,格式为: typename name[ = default] typename ... name:(可变参数模板) |
3 | 模板模板形参 | 有两个"模板",这个比较复杂,暂没有研究过 |
函数(function-declaration):
这部分和函数声明差不多,只不过函数的返回值类型和参数类型都可以是模板形参。另外对函数的各种修饰(inline、constexpr等)需要加在function-declaration前,而不是template前。
接下来通过函数模板的例子更好的理解下函数模板。
函数模板的定义:
/* 函数模板:1 * 一个类型参数的模板 * inline修饰的函数模板 */templateinline void swap_template(T& t1, T& t2){ T temp = t1; t1 = t2; t2 = temp;}/* 函数模板:2 * 多个类型参数的模板 */templateT2 print_template(T1 arg1, T2 arg2){ std::cout << arg1 << " " << arg2 << std::endl; return arg2;}/* 函数模板:3 * 数组排序模板 * 利用选择排序,对数组进行从大到小的排序 */templatevoid sort_template(T arr[], int iLen){ for (int i = 0; i < iLen; i++) { /* 最大数的下标 */ int iMax = i; for (int j = i + 1; j < iLen; j++) { if (arr[iMax] < arr[j]) { iMax = j; } } /* 如果最大数的下标不是i,交换两者 */ if (iMax != i) { swap_template(arr[iMax], arr[i]); } }}/* 函数模板:4 * 打印数组信息模板 */templatevoid print_array_template(T arr[], int iLen) { for (int i = 0; i < iLen; i++) { std::cout << arr[i] << " "; } std::cout << std::endl;}/* 函数模板:5 * 非类型形参情况1 */templatevoid print_template_multi(const T& arg1){ for (int i = 0; i < iCount; i++) { std::cout << arg1 << std::endl; }}
函数模板的使用:
void test_func_template_1(void){ /* 函数模板:1 */ int swap_para_1 = 20; int swap_para_2 = 30; swap_template(swap_para_1, swap_para_2); /* 函数模板:2 */ print_template(100.234, 800); double swap_para_3 = 1.23; double swap_para_4 = 4.56; double iRet3 = print_template(swap_para_3, swap_para_4); /* 函数模板:3 */ char testSortChar[10] = { 'a','1','b','d','c','2','0','5','3','4' }; sort_template(testSortChar, 10); /* 函数模板:4 */ print_array_template(testSortChar, 10); /* 函数模板:5 */ print_template_multi<:string>("func template 5");}int main(void){ /* 测试函数模板 */ test_func_template_1(); system("pause"); return 0;}
03 函数模板实例化
C++Primer中模板实例化的定义:函数模板指定了怎样根据一组或更多实际类型或值构造出独立的函数,这个构造过程被称为模板实例化。说白了就是只有当用具体类型去替换形参T时,才会生成具体函数,该过程叫是函数模板的实例化。函数模板实例化时机,分别是在它被调用时或对其取地址,被调用时实例化就不必说了,那么对其取地址时是什么样的情况呢?
看下图例子就是取地址时实例化的情况:
函数模板实例化又分为隐式实例化和显示实例化,详细如下:
隐式实例化:
当函数模板被调用,且在之前没有显式实例化时,即发生函数模板的隐式实例化。如果模板实参能从调用的语境中推导,则不需要提供。
显示实例化:
在函数模板定义后,我们可以通过显式实例化的方式告诉编译器生成指定实参的函数。显式实例化声明会阻止隐式实例化。如果我们在显式实例化时,只指定部分模板实参,则指定顺序必须自左至右依次指定,不能越过前参模板形参,直接指定后面的,请理解下图所示:
其实我们上面使用函数模板的例子中也包括了这两种实例化方式:
我们看下函数模板2的另一种使用方式:
04 函数模板实参推演
C++Primer中关于实参推演的定义:用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演。也就是完全显示指定模板实参的时候就没有实参推演,不完全显示实例化模板的时候存在实参推演。我们还是以上文提到的min_template函数模板为例来理解下实参推演:
函数模板 min_template()的函数参数是一个引用,它指向了一个T类型的数组,这样的显示调用和隐式调用是正确的,但是如果像下面这样的调用是错误的:
另外在模板实参推演期间决定模板实参的类型,但是编译器不考虑函数模板实例的返回类型
例如下面的调用:
要想成功地进行模板实参推演,需要类型匹配,但是实参类型匹配时三种类型转换是允许的,分别是左值转换、限定转换、到一个基类的(转换该基类根据一个类模板实例化而来 )。
(1)左值转换包括从左值到右值的转换、从数组到指针的转换、从函数到指针的转换。请看下面定义和使用模板的例子:
如果大家对左值和右值不理解的话,请参见简书的这篇文章:
https://www.jianshu.com/p/94b0221f64a5
(2)限定修饰转换把const 或volatile 限定修饰符加到指针上,这个其实很好理解,就像函数参数有const修饰,我传入一个非const的参数一样。
(3)基类转换
如果函数参数的类型是一个类模板,且如果实参是一个类,它有一个从被指定为函数参数的类模板实例化而来的基类,则模板实参的推演就可以进行。这句话很绕,后续我们讲到类模板的时候详细介绍。
所以模板实参推演的通用算法如下:
1 .依次检查每个函数实参,以确定在每个函数参数的类型中出现的模板参数;
2 .如果找到模板参数,则通过检查函数实参的类型 推演出相应的模板实参;
3 .函数参数类型和函数实参类型不必完全匹配,下列类型转换可以被应用在函数实参上以便将其转换成相应的函数参数的类型;
4.如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同 ;
05 函数模板特化
当函数模板需要对某些类型进行特别处理,这称为函数模板的特化。函数模板特化时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。函数模板特化的本质是实例化一个模板,而非重载它。函数模板特化的定义,先是关键字 template 和一对尖括号 <>,然后是函数模板特化的定义,该定义指出了模板名、被用来特化模板的模板实参,以及函数参数表和函数体。我们来看一下具体例子:
/* 普通函数模板与特化模板 * 特化模板以template<>开头,并通过名称来指出类型 * 特化模板优先于常规模板 */class CITWang{public: CITWang(std::string name, int age) { this->m_strName = name; this->m_iAge = age; } /* 此处重载==运算符也可以使用模板 */ //bool operator==(const CITWang& t) { // if ((t.m_strName == this->m_strName) && (t.m_iAge == this->m_iAge)) // { // return true; // } // else // { // return false; // } //} std::string m_strName; int m_iAge;};/* 比较函数模板 */templatebool compare_template(T a, T b){ if (a == b) { return true; } else { return false; }}/* 比较函数特化模板 */template<> bool compare_template(CITWang p1, CITWang p2){ if (p1.m_strName == p2.m_strName && p1.m_iAge == p2.m_iAge) { return true; } else { return false; }}
/* 验证普通函数模板和te化模板 */ CITWang p1("Chen", 30); CITWang p2("Chen", 30); CITWang p3("He", 30); bool bIs1 = compare_template(p1, p2); bool bIs2 = compare_template(p3, p2); print_template(bIs1, bIs2); bool bIs3 = compare_template(30, 20); bool bIs4 = compare_template(30, 30); print_template(bIs3, bIs4);
其实函数模板特化意义不是很大,因为我们可以通过其他方式来适配普通模板,比如上述例子中的重载"=="运算符,我们就不比特化模板。另外如果我们使用特化模板,函数模板及其特化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后在是这些模板的特化版本。
06 函数模板重载
函数模板之间,普通函数和模板函数之间都可以重载。编译器会根据调用时提供的函数参数,调用最佳匹配版本。在匹配度上,一般按照如下顺序考虑:
最符合函数名和参数类型的普通函数
特殊模板(具有非类型形参的模板,即对T有类型限制)
普通模板(对T没有任何限制的)
通过类型转换进行参数匹配的重载函数
另外在调用一个模板实例时,重载的函数模板可能会导致二义性,消除调用的二义性的惟一方法是显式指定模板实参。
07 可变参函数模板
在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“...”,省略号“...”的作用有两个:
声明一个参数包,这个参数包中可以包含0到任意个模板参数;
在模板定义的右边,可以将参数包展开成一个一个独立的参数;
我们来看下定义和使用的实例:
/* 可变参函数模板,T叫模板参数包,args叫函数参数包 */template<class ... T>void func(T ... args) { std::cout << "num = " << sizeof...(args) << std::endl;}
/* OK:args不含有任何实参 */ func(); /* OK:args含有一个实参:int */ func(1); /* OK:args含有两个实参int和double */ func(2, 1.0);
上面仅是简单的定义和示例,其实可变参数模板主要用来处理既不知道要处理的实参的数目,也不知道它们的类型时的场景。那么我们怎么使用它呢?通常有两种方法,递归调用和循环调用;
递归调用:通过递归来遍历所有的实参,需要给出终止递归的条件,示例如下:
//可变参数模板函数使用方法1:递归调用,每次将可变参数规模变小直到为0template<typename T>void print(const T& x) { std::cout << "last print,x:" << x << std::endl;}template<typename T, typename... args>void print(const T& x, const args&... rest) { std::cout << "current print x:" << x << std::endl; std::cout << "current args lenth:" << sizeof...(rest) << std::endl; print(rest...);}
/* 测试可变参模板:递归调用测试 */ print("test print1", 2, "abc");
循环调用,示例如下:
//可变参数模板函数使用方法2:循环调用(此例有弊端,需要不定个数参数的类型一致)template<typename... args>void print2(args&&... li) { for (auto x : { li... }) { std::cout << x << std::endl; }}
/* 测试可变参模板:循环调用测试 */ print2(3, 2, 1);
据C++primer上说,可变参数模板函数一般用来将它的参数转发给其他函数,后续我们讲C11完美转发的时候在做详细介绍。
08 总结
- 函数模板是泛型编程在C++中的应用方式之一;
- 函数模板能够根据实参对参数类型进行推导;
- 函数模板支持显示的指定参数类型;
- 函数模板是C++中重要的代码复用方式;
- 函数模板通过具体类型产生不同的函数;
- 函数模板可以定义任意多个不同的类型参数;
- 函数模板中的返回值类型必须显示指定;
- 函数模板可以像普通函数一样重载;
- 本文没有对函数模板的编译进行介绍,如有需要了解,请参考C++primer中的介绍;