本系列是《C++Template》(作者:David Vandevoorde, Nicolai M. Josuttis)的学习总结。
1:函数模板(Function Template)
所谓函数模板是由参数表示的一系列的函数。函数模板可以被不同的类型参数所调用,使用时和普通的函数功能一样,不同的是函数模板在定义的时候参数的类型是未知的。
1.1.、例子,模板定义
template <typename T>
inline T const& max (T const& a, T const& b)
{
// if a < b then use b else use a
return a<b?b:a;
}
以上代码的功能是求两个值中的最大值,参数的类型是不确定的,在此声明为模板参数T。
模板参数的声明如下:
template < comma-separated-list-of-parameters >
上面的例子中的参数是,由一对<>扩起来。typename 是关键字,T是类型参数标识,如果有多个参数标识符用”,”分隔。当然参数标识符也可以使用其它的标识符,只是习惯上用T(T是Type的首字母)。
注:有的地方关键字typename也会用class替代两者没有差别,但是在本系列学习笔记中用typename,关键字class只用来表示类。
1.2、模板的使用
#include <iostream>
#include <string>
#include "max.hpp"
int main()
{
int i = 42;
std::cout << "max(7,i): " << ::max(7,i) << std::endl;
double f1 = 3.4;
double f2 = -6.7;
std::cout << "max(f1,f2): " << ::max(f1,f2) << std::endl;
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1,s2): " << ::max(s1,s2) << std::endl;
}
结果如下:
max(7,i): 42
max(f1,f2): 3.4
max(s1,s2): mathematics
上面的例子中函数模板max()被调用了三次,每次调用的时候使用的参数类型是int,double和string
注:上面调用max()前面都使用了::,这是因为在标准模板库中也有个std::max()的模板函数,使用::可以确保max()使用的是我们上面定义的max()函数。
上面的过程看起来好像是由一个max()函数处理三个不同类型的参数,但是实际上并不是这样的。我们上面的函数模板max(),被编译器根据调用时的不同类型产生了三个函数体。
使用int的函数体:
inline int const& max (int const& a, int const& b)
{
// if a < b then use b else use a
return a<b?b:a;
}
其它两种类型的函数体:
const double& max (double const&, double const&);
const std::string& max (std::string const&, std::string const&);
由具体的类型取代模板参数的工程被称为实例化。实例化的结果就是产生了执行是调用的真正的函数实例。
注:只要函数模板被调用就会实例化
试图使用某个类型实例化一个模板函数时,如果该类型不支持模板中的操作,就会产生编译错误,例如:
std::complex<float> c1, c2; // doesn't provide operator <
…
max(c1,c2); // ERROR at compile time
上面的例子中,由于比较运算符“<”不支持复数,因此发生编译错误。
在我们编译程序时,实际过程模板会被编译两次。
- 首先,对模板代码检查语法(如缺少分号)等语法错误。
- 然后,实例化时,即调用模板函数时检查模板函数中的操作是否支持该类型。
2、参数推导(Argument Deduction)
在上面的例子中,当调用max()时模板参数是根据调用时的参数推导出来的。如上面传入的是int类型,C++编译器就会推导出T必须是int。自动推导是没有类型转换的,因此传入入的类型必须严格匹配。
template <typename T>
inline T const& max (T const& a, T const& b);
…
max(4,7) // OK: T is int for both arguments
max(4,4.2) // ERROR: first T is int, second T is double
对于第二个max()的调用,由于第一个参数是int,第二个double。因此编译时候会产生错误。
这种错误,有三种方法可以及解决:
1、转换参数,让两个参数严格匹配
max(static_cast<double>(4),4.2) // OK
2、显示指定类型T
max<double>(4,4.2) // OK
3、定义函数模板时指定两个不同类型的参数
template <typename T1,typename T2>
inline T const& max (T2 const& a, T2 const& b)
3、模板参数(Template Parameters)
函数模板有两种类型的参数:
1、模板参数,使用尖括号”<>”声明在函数模板名之前。
template <typename T> // T is template parameter
2、调用参数,使用圆括号”()”声明在函数模板名之后
… max (T const& a, T const& b) // a and b are call parameters
模板参数的个数可是任意个,但是不能为模板参数指定默认值。
例如,可以为函数模板max()指定两个不同类型:
template <typename T1, typename T2>
inline T1 max (T1 const& a, T2 const& b)
{
return a < b ? b : a;
}
…
max(4,4.2) // OK, but type of first argument defines return type
上面的例子看起来很好,但是有问题。首先,返回类型必须被声明。如果返回类型是其中的一个模板参数类型,另一个参数类型就可能被转换成返回的类型。另外一个问题,把第二个类型转换为第一个类型会产生局部临时对象,那么就不能使用引用的方式(by reference)传回结果。因此上面的例子中,返回的类型是T1 而不是 T const&
由于调用参数(call parameters )是由模板参数(template parameters)构造的,所以两者是相关的。我们把这种概念称为:函数模板参数推导。有了推导,就可以像调用普通函数那样调用函数模板。
如之前的例子,调用时为函数模板显示指定类型
template <typename T>
inline T const& max (T const& a, T const& b);
…
max<double>(4,4.2) // instantiate T as double
当模板参数和调用参数没有直接关系,且编译器也无法推导出模板参数是,就需要明确的指定模板参数了。如,可以为max()指定第三个模板参数类型作为返回参数类型。
template <typename T1, typename T2, typename RT>
inline RT max (T1 const& a, T2 const& b);
然而,推导机制并不会对返回类型进行匹配,而且模板参数RT也不在调用参数中。因此,编译器无法推导出RT,调用时就必须显示指定类型,如下:
template <typename T1, typename T2, typename RT>
inline RT max (T1 const& a, T2 const& b);
…
max<int,double,double>(4,4.2) // OK, but tedious
上面的例子,调用时要么不需要指定参数完全由编译器推导,要么就要把所有的参数多显示指定了。
当然,还有一种方法只明确的指定第一个模板参数,其它的参数由编译器自动推导。
如下:RT要放在第一个参数的位置。
template <typename RT, typename T1, typename T2>
inline RT max (T1 const& a, T2 const& b);
…
max<double>(4,4.2) // OK: return type is double
4、函数模板重载(Overloading Function Templates)
和普通的函数一样,函数模板也是可以重载的。
函数重载:不同的函数的定义可以有相同的函数名,当函数被调用的时候由编译器判断使用哪个函数。
如下,函数模板重载的例子:
// maximum of two int values
inline int const& max (int const& a, int const& b)
{
return a<b?b:a;
}
// maximum of two values of any type
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a<b?b:a;
}
// maximum of three values of any type
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
return max (max(a,b), c);
}
int main()
{
::max(7, 42, 68); // calls the template for three arguments
::max(7.0, 42.0); // calls max<double> (by argument deduction)
::max('a', 'b'); // calls max<char> (by argument deduction)
::max(7, 42); // calls the nontemplate for two ints
::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(7, 42) // both int values match the nontemplate function perfectly
但是,如果模板更匹配就使用模板的实例化函数,如:
max(7.0, 42.0) // calls the max<double> (by argument deduction)
max('a', 'b'); // calls the max<char> (by argument deduction)
调用时可以使用空的模板参数列表”<>”,这种形式告诉编译器必须使用从函数模板的实例,且模板参数由调用参数自动推导。
max<>(7, 42) // calls max<int> (by argument deduction)
由于模板是不能进行自动类型转换的,而普通函数是可以自动类型转换,所有最后一个调用的是非模板函数。
max('a', 42.7) // only the nontemplate function allows different argument types
一个更有用的例子是,为指针类型和C-style的字符串重载max()模板
#include <iostream>
#include <cstring>
#include <string>
// maximum of two values of any type
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
// maximum of two pointers
template <typename T>
inline T* const& max (T* const& a, T* const& b)
{
return *a < *b ? b : a;
}
// maximum of two C-strings
inline char const* const& max (char const* const& a,
char const* const& b)
{
return std::strcmp(a,b) < 0 ? b : a;
}
int main ()
{
int a=7;
int b=42;
::max(a,b); // max() for two values of type int
std::string s="hey";
std::string t="you";
::max(s,t); // max() for two values of type std::string
int* p1 = &b;
int* p2 = &a;
::max(p1,p2); // max() for two pointers
char const* s1 = "David";
char const* s2 = "Nico";
::max(s1,s2); // max() for two C-strings
}
上面的所有的重载函数多是传引用(by reference)。一般来讲,重载函数之间有这明显的差异,或者是参数的个数不同,或者是参数的类型明显不同,如果差异不够明显可能会出现一些意想不到的问题。
如下,以传值(by value)的方式重载一个传引用(by referenc)的max(),就无法使用三个参数的max()函数来取得C-style字符串的最大者:
// maximum of two values of any type (call-by-reference)
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
// maximum of two C-strings (call-by-value)
inline char const* max (char const* a, char const* b)
{
return std::strcmp(a,b) < 0 ? b : a;
}
// maximum of three values of any type (call-by-reference)
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
return max (max(a,b), c); // error, if max(a,b) uses call-by-value
}
int main ()
{
::max(7, 42, 68); // OK
const char* s1 = "frederic";
const char* s2 = "anica";
const char* s3 = "lucas";
::max(s1, s2, s3); // ERROR
}
上面的例子中,由于C-style字符串的max(a,b)重载函数创建了一个新的局部值,而改值又以引用的方式传回,就产生了错误。
上面的例子是细微的重载引发的非预期的行为。当函数调用时,如果不是所有的重载函数都在当前的范围可见,那么上述的问题可能发生,也可能不发生。如下,
// maximum of two values of any type
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a<b?b:a;
}
// maximum of three values of any type
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
return max (max(a,b), c); // uses the template version even for ints
} // because the following declaration comes
// too late:
// maximum of two int values
inline int const& max (int const& a, int const& b)
{
return a<b?b:a;
}
由于int类型的max()非模板函数定义在了调用之后,那么“return max (max(a,b), c); 调用的是函数模板的int实例。
5、总结
1、函数模板可以针对不同的模板参数定义一系列的函数。
2、函数模板根据调用参数类型,实例化不同类型的函数。
3、调用时可以显示指定目标参数。
4、函数模板也可以重载。
5、函数模板重载是,不同的重载函数之间最好存在明显的差异。
6、必须保证所有函数都定义在被调用之前。