本文章主要介绍C++的函数模板,方便初学者建立函数模板的概念,同时理解编译器如何根据提供的函数实参(可以简单理解为函数入参)来判断函数的类型,类模板相关介绍见 C++ 类模板。
1 定义
模板本身不是类或函数,可以将模板看做编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化。
函数模板定义以template
开始,后跟一个模板参数列表,模板参数列表可以为多个,如<typename T, typename U>
,也可以将typename
替换成class
,早期程序员喜欢使用class
,可以混写为<typename T, class U>
。最终可写为template <typename T, typename U>
或者template <typename T, class U>
。注意不能写成<typename T,U>
。
举例:
template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) {
return -1;
}
if (v2 < v1) {
return 1;
}
return 0;
}
2 实例化函数模板
实例化函数模板是通过使用具体值替换模板实参,从模板中产生函数的过程。当我们调用一个函数模板时,编译器用函数实参为我们推断模板实参。也就是说,当我们调用compare
时,编译器使用实参的类型来确定绑定到模板参数T
的类型。
// 实例化出int compare(const int& v1, const int& v2)
cout << compare(1 , 0) <<endl; // T 为int
//实例化出int compare(const vector<int>& v1,const vector<int>& v2)
vector<int> vec1{1,2,3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T为vector<int>
注意上述使用compare
的类型的int
型和vector<int>
本身是有<
比较运算符的,因此可以这么实例,如果传入的类型没有定义<
比较运算符,是无法进行比较的。
特别的,T
除了作为函数的入参外,也可以为函数的返回类型或函数内部变量。
template <class T>
T foo(T* p ) {
T temp = *p;
return tmp;
}
3 模板的编译
在编写代码时,应该尽量减少对实参类型的要求,使得模板更加通用。
当编译器遇到一个模板定义时,并不生成代码。只有我们实例化(而不是定义)出一个模板的特定版本时,编译器才会生成代码,这一特性影响了我们如何组织代码以及错误何时被检测发现。
Note:
函数模板和类模板成员函数的定义通常放在头文件中。
4 模板参数和模板实参
模板参数是指:位于模板声明或定义内部,关键字template
后面所列举的名称(如上面出现的T
)。
模板实参是指:用来替换模板参数的各个对象,如上面提到的替换T
的1,0
,或者vec1,vec2
。
5 模板实参推断
从函数实参来确定模板实参的过程称为模板实参推断。在推断过程中,编译器智能根据函数调用的实参类型来寻找模板实参类型。例如compare
函数可以推断出T
的类型为int
,上述的这个过程称为函数模板的隐式实参(可以这么近似理解,C++primer中没有这么说)。在某些情况下,编译器无法推断出模板实参的类型,此时需要我们显式定义出来,这个过程称为函数模板的显式实参(C++ primer中是这么说的)。
// 编译器此时是无法推断出T1,它没有出现在函数参数列表中,因此调用的时候需要显式实参
template <class T1, typename T2, class T3>
T1 sum(T2 v2, T3 v3) {
}
在上面这个例子中,调用时编译器无法推断出T1
的类型,因此我们要告诉编译器T1
是什么类型,此时就是显式模板实参。我们提供显式模板实参的方式与定义类模板实例(例如vector<int> vecInt
)的方式相同。显式模板实参在尖括号中给出,位于函数名后,实参列表之前。
int i;
long value;
auto val3 = sum<double>(i,value); // 最终被实例为了double sum(int, long)
上面代码块T1
被显式指定,T2
和T3
被编译器从i
和value
中推断出来。
显式模板实参需要按照从左到右的顺序与对应的模板参数对应起来。第一个模板实参(double
)与第一个模板参数(T1
)对应,第二个模板实参与第二个模板参数对应。只有尾部参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数中推断出来。
//下面是一种不好的设计,设计时是T1,T2,T3,使用时的顺序是T3,T2,T1
template <typename T1, typename T2, class T3>
T3 otherSum(T2, T1);
那我们在指定实参时,需要全部写出来。
int i;
long value;
auto val3 = otherSum<double>(i, value); // 错误,这种格式系统不能推断出来,报错
auto val2 = otherSum<double, int, long>(i, value); //这样写,显式指定了所有的三个参数类型
在ROS
的订阅话题部分使用的subscribe
,就是使用函数模板的显式实参。举例如下,截取LIO-SAM
中的代码:
ros::NodeHandle nh;
subImu = nh.subscribe<sensor_msgs::Imu>(imuTopic, 2000,
&ImageProjection::imuHandler, this, ros::TransportHints().tcpNoDelay());
其中subscribe
为一个函数,第一个参数<sensor_msgs::Imu>
为函数模板的显式实参。写这篇文章的目的是在看上面这行代码时,产生了困惑,之后找到了上面这个函数的定义位置,见下面的代码。其中M
就是被sensor_msgs::Imu
实例化了。subscribe
的定义链接: ros::NodeHandle::subscribe
template<class M >
Subscriber subscribe (const std::string &topic,
uint32_t queue_size,
const boost::function< void(const boost::shared_ptr< M const > &)> &callback,
const VoidConstPtr &tracked_object=VoidConstPtr(),
const TransportHints &transport_hints=TransportHints())
6 重载函数模板
和普通函数相同,函数模板也可以被重载。相同的函数名称可以具有不同的函数定义;于是,当使用函数名称进行函数调用的时候,C++编译器必须决定要调用哪个候选函数。下面给个重载的例子:
// 求两个int值的最大值
inline int const& max(int const& a, int const& b) {
return a < b ? b : a;
}
// 求2个任意类型值中的最大值
template <typename T>
inline T const& max(T const& a, T const& b) {
return a < b ? b : a;
}
// 求3个任意类型值中的最大者
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); // 1 调用三个参数的函数,里面的函数再去调用非模板函数
::max(7.0,42.0); // 2 调用max<double>模板函数
::max('a', 'b'); // 3 调用max<char>模板函数
::max(7, 42); // 4 调用int重载的函数,注意不是模板函数
::max<>(7,42); // 5 调用max<int>模板函数
::max('a', '42.7'); // 6 调用int重载的函数,注意不是模板函数
}
上面的函数中,解释下::
、内联函数、const&
。
::
:调用的是全局函数,::
前面是空的代表的是全局作用域。
内联函数
:以 inline
修饰的函数,编译时C++编译器在调用内联函数的地方直接展开,不再像其他函数一样去调用,没有调用建立栈帧的开销。内联函数一般代码量少,没有for
循环,需要频繁调用,提升程序运行的效率。
const&
:指的是返回常值引用,int const&
也可以写成const int&
,效果是一样的,表示的意思是返回值是常值引用的整型。注意,当返回值是引用类型时,返回的变量应该是真实存在的,不是一个局部变量,这里简单再举个例子,下面的temp_str
类型就不能这样返回,因为temp_str
是一个临时变量,函数入参是以拷贝方式传入的,函数入参如果改为string& temp_str
,下面的函数就可以编译通过了。
string& Test(string temp_str) { // 拷贝传值,temp_str为函数内部的局部变量
return temp_str; // 【错误】返回局部变量,函数结束后不存在,将引起内存问题
}
接下来继续来说重载函数模板,一个非模板函数可以和一个同名的函数模板同时存在,而且函数模板还可以被实例化为这个非模板函数。当非模板函数和同名的函数模板,其他条件相同时,编译器会优先调用非模板函数,而不会从函数模板中产生一个实例,如例子4
。
然后当模板可以找到一个匹配更好的函数时,编译器会选择模板,如第2
和3
个例子,调用了max<double>
和max<char>
(如果没有模板函数,例子2
和3
会调用非模板函数)。也可以显式指定让编译器调用模板函数,如例子5
。
最后,模板函数是不允许自动转换类型的,但普通函数会自动类型转换,所以例子6
调用的是非模板函数。
类模板的介绍见链接: C++ 类模板。
参考书籍:《C++ Primer 第5版 》和《C++ Templates 中文版》