C++ 函数模板


本文章主要介绍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)。
模板实参是指:用来替换模板参数的各个对象,如上面提到的替换T1,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被显式指定,T2T3被编译器从ivalue中推断出来。
显式模板实参需要按照从左到右的顺序与对应的模板参数对应起来。第一个模板实参(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
然后当模板可以找到一个匹配更好的函数时,编译器会选择模板,如第23个例子,调用了max<double>max<char>(如果没有模板函数,例子23会调用非模板函数)。也可以显式指定让编译器调用模板函数,如例子5
最后,模板函数是不允许自动转换类型的,但普通函数会自动类型转换,所以例子6调用的是非模板函数。

类模板的介绍见链接: C++ 类模板
参考书籍:《C++ Primer 第5版 》和《C++ Templates 中文版》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值