C++语法|函数模版?模版函数?深入理解函数与模板与分文件编写

本文主体内容仍然来自《CPU眼里的C/C++》

CPU眼里的模板

先说三条结论

  • 其实CPU对模板是无感的,模板本质上是编译器根据我们提供的脚本自动补充代码,设计的数据类型越多,代码的版本也就越多。
  • 编译器自动补充的代码对程序员来说是不可见的。所以,在单步调试的时候,会出现源代码无法一一对应的问题,模板的相关代码,往往只能黑盒测试,很难找到有效的调试方法。
  • 在对类进行模板化操作的时候,如果涉及数学、逻辑运算,由于编译器往往无法提供默认的运算符操作,就需要程序员手动为类重载这些运算符,避免可能的编译错误。

我们想写一个简单的加法运算:

int add(int x, int y) {
	return x + y;
}
float add (float x, float y) {
	return x + y;
}

现在我们可以使用函数的模板化方法来进行简化:

template<typename T>
T add(T x, T y) {
	return x + y;
}

通过编译器我们可以看到:

我们可以明显看到,我们的模板函数生成的int add (int, int)和普通函数add(int, int)是一模一样的,他们的汇编语法完全一致!

在这里能完全体会到结论中的:

其实CPU对模板是无感的,模板本质上是编译器根据我们提供的脚本自动补充代码,设计的数据类型越多,代码的版本也就越多。


现在我们定义一个简单的类A,然后对类A进行模版化的加法运算:

class A {
public:
	int a;	
};
template<typename T>
T add(T x, T y) { return x + y; }
int main () {
	A x, y;
	add<A> (x, y);
} 

编译错误!因为我们没有重载类A的“+”运算符,所以无法进行模板中定义的加法运算,所以重载+运算符:

class A {
public:
	int a;
	A operator+ (A const &y) {
		A res;
		res.a = this->a + y.a;
		return res;
	}
};

由此编译通过,所以模板类也只是编译器代替程序员做了一些简单、重复的工作。
这里体现了我们结论中的:

在对类进行模板化操作的时候,如果涉及数学、逻辑运算,由于编译器往往无法提供默认的运算符操作,就需要程序员手动为类重载这些运算符,避免可能的编译错误

函数模板

模板的一个重要意义,就是对类型也可以进行参数化

函数模板和模版的实例化

这里举例一个典型的函数模板:

template<typename T> //定义一个模版参数列表
bool compare(T a, T b) 
	cout << "template compare" << endl;
    return a > b;
}

我们之前定义的那个compare是一个函数模板,在我们调用的时候肯定是要调用一个函数名称。所以在调用的时候语法如下:
模版名 + 参数列表 = 函数名

int main () {
	compare<int>(10, 20);
	return 0;
}

NOTE:
在我们函数调用点才进行实例化;
函数模版是不会进行编译的(因为编译器还不知道它的类型),只有在模版实例化的时候才进行编译!

模版函数

我们把compare(10, 20)称为函数的调用点。
从原模版在函数调用点,编译器用用户指定的类型,从原模版实例化一个函数代码出来

bool compare<int>(int a, intb) {
	return a > b;
}

我们把上面这个函数称为模版函数。
真正要参与代码编译的函数是模版函数。

模版函数才是被编译器所编译的

模版的实参推演

还是对于该模版函数:

template<typename T> //定义一个模版参数列表
bool compare(T a, T b) 
	cout << "template compare" << endl;
    return a > b;
}

我们有时候会发现,对于定义的函数模版,我们直接这样用:

int main () {
	compare<int>(10, 20);
	compare(20, 30);
	return 0;
}

我们直接使用函数名称compare(20, 30)竟然也能完成调用,这就是模版的实参推演:可以根据用户传入的实参的类型,来推导出模版类型参数的具体类型
这个时候编译器会延用之前的那份模版函数,而不会额外再生成一份,再生成一份就叫函数的重定义了,会导致链接错误,所以编译器会延用compare<int>(10, 20);生成的那份函数:

bool compare<int>(int a, intb) {
	return a > b;
}

如果出现这样的情况呢?

compare(30, 40.5);

我们传入了一个整数,然后传入一个浮点数,编译器通过30推导出T是整型,通过40.5推导出T是一个double类型,那么T到底是什么类型呢?编译器不知道,所以就直接报错。

针对这种情况,我们可以定义两个类型参数,T1 和 T2,这样就各自不影响了。
当然了我们也可以这样调用:

compare<int>(30, 40.5);

编译器会为我们把double类型转换为整型。

模板类型参数和非类型参数

1.类型参数

我们使用 typename 或者 class 来声明的就是模板类型参数
比如说:

template<typename T1, typename T2>

2. 非类型参数

这里假设我们定义一个数组,并且对其进行冒泡排序。

int main () {
    int arr[] = {12, 5, 7, 89, 32, 21, 35};
    const int size = sizeof(arr) / sizeof(arr[0]);
    sort(arr, size);
    return 0;
}

现在我们来实现这个sort函数,
在这里任何类型我们都可以对其进行冒泡排序,不仅仅是整数,所以我们使用模板typename T,再一个我们对该模板函数定义一个模板类型参数int SIZE

template<typename T, int SIZE>
void sort(T *arr){
	for (int i = 0; i < SIZE - 1; ++i) {
		for ( int j = 0; j < SIZE - 1 - i; ++j) {
			if (arr[j] > arr[j + 1]) {
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

要注意的是非类型参数都是常量,只能使用,而不能修改
打印最后结果:

int main () {
    int arr[] = {12, 5, 7, 89, 32, 21, 35};
    const int size = sizeof(arr) / sizeof(arr[0]);
    sort<int, size>(arr); // 这里的size必须是常量const
    for (int val : arr) cout << val << " ";
    return 0;
}

再次注意!!!
模板的非类型参数是一个常量!只能使用不能修改,而且必须是整数类型(整数或者地址/引用都可以)
至于为什么叫非类型参数呢,因为我们在定义的时候已经指明了类型。<typename T, int size>

模板的特例化(专用化)

还是之前的函数模版:

template<typename T> 
bool compare(T a, T b) 
	cout << "template compare" << endl;
    return a > b;
}

我们这样进行调用:

int main () {
	compare("aaa", "bbb");
	return 0;
}

这里也是能够进行比较的,因为实现了函数模版的实参推演。编译器为我们生成了:

bool compare<const char*>(const char* a, const char* b) {
	return a > b;
}

但其实,我们这里比较的是 a 和 b的地址大小,这样是没有意义的。
我们希望能按照字典序来比较 a 和 b的大小。
所以我们需要特例化模版,我们需要在代码中显示得声明该模版函数

//针对conmpare函数模版,提供const char* 类型的特例化
template<>
bool compare<const char*>(const char* a, const char* b) {
	cout << "compare<const cahr*>" << endl;
	return strcmp(a, b) > 0;
}
int main () {
	compare("aaa", "bbb");
}

函数模版、函数模版的特例化和非模版函数的重载关系

刚才我们是实现了const char * 类型的特例化,其实我们还可以这样写:

//函数模版
template<typename T> 
bool compare(T a, T b) 
	cout << "template compare" << endl;
    return a > b;
}
//函数模版的特例化
template<>
bool compare<const char*>(const char* a, const char* b) {
	cout << "compare<const cahr*>" << endl;
	return strcmp(a, b) > 0;
}
//非模版函数-普通函数
bool compare(const char* a, const char* b) {
	cout << "nomal compare" << endl;
	return strcmp(a, b) > 0;
}

其实我们还并不应该讨论他们为重载,因为至少他们的参数列表是一样的。但是我们来比较一下,他们三个其实根本就长得不一样:

//函数模版
template<typename T> 
bool compare(T a, T b);
//函数模版的特例化
template<>
bool compare<const char*>(const char* a, const char* b) ;
//非模版函数-普通函数
bool compare(const char* a, const char* b);

个人认为确实不能叫重载,我们我们在调用的时候,函数名的书写根本就不一样,只不过参数列表是一样的。

需要注意的是,他们三个是能够共存的。
当我们调用:
compare("aaa", "bbb");的时候,编译器会讲compare当成普通函数;
compare<const char*>("aaa", "bbb");,编译器无论如何都不能把它当作函数名了,而是一个特例化的模版函数

总结:编译器优先把compare处理成函数的名字,如果没有的话,才会去当成模版函数来处理。

分文件编写

由于函数(类)模版的代码是不会参与编译的,只有在模版代码调用的时候编译器才能进行编译。

所以:

模版代码是不能在一个文件中定义,在另一个文件中使用的,模版代码调用之前,一定要看到模版定义的地方,这样的话,模版才能够进行正常的实例化,产生能够被编译器编译的代码。所以模版代码都是放在头文件当中的,然后在源文件当中直接进行#include(预处理器会直接把include的头文件展开)包含

当然了你如果一定要分文件写也是可以的,直接

//file 1 中直接显示声明
template bool compare<int> (int, int);
template bool compare<double> (double, double);

//
int main () {
	compare<int>(10, 20);
	compare<double>(10.1, 20.2);
}

这是在告诉编译器,进行指定类型的模版实例化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值