前言
有些人提到C++模板就会下意识地觉得可怕、看不懂、避而远之。其实模板并不复杂,而且熟练后可以用在日常工作中,可以帮助我们重用代码,让代码更简洁、易读、可维护。希望这个系列的文章,能够让更多人发现模板的魅力,帮助大家写出更高质量的代码。
我们为什么需要模板
我们有时候会遇到这样的情况:同样的函数,我们要为不同的类型写不同的版本,内容与逻辑都是一摸一样的,只有他们的类型不一样。比如我们写一个max函数,传入两个数字,返回大的数字。很自然地,两个参数的类型和返回的类型必须是相同的。如果不使用模板,我们需要使用函数重载,为不同的类型写不同的函数:
int max(int a, int b) { return a < b ? b : a; } float max(float a, float b) { return a < b ? b : a; }
这里我只写了2个函数,实际上short, long, unsigned, double等等类型都需要专门的max函数,结果就是需要写十几个几乎一摸一样的代码。如果函数功能更复杂一些,函数实现需要更多行,就会出现大量冗余重复的代码,而且不容易维护,很容易出错。这个时候如果我们能够根据特定的模板批量生成一系列代码,将会方便很多。为此,我们可以使用C++中的模板
什么是模板
顾名思义,模板就是编译器生成代码用的模子。模板有两类,函数模板和类模板(C++14开始出现了变量模板,不过不在此讨论)。如果想要生成函数代码,则需要用函数模板,如果想要生成类定义,则需要用到类模板。这篇文章会先介绍函数模板,下篇文章再介绍类模板。
函数模板
我们可以为上面的一系列max函数写一个函数模板。
template<typename T> T max(T a, T b) { return a < b ? b : a; }
我们暂时不细说语法,先看一看大致的样子,其实函数模板的长相和普通的函数是很像的。
好了,我们已经定义了一个函数模板,那么怎么去生成函数代码?事实上我们不需要做额外的事情,如果我们使用了max函数,编译器就会自动帮我们生成对应类型的代码。函数模板的使用方式很简单,只需要在模板的名字后面写一对尖括号,尖括号内写上实参列表就可以使用了。
double d = max<double>(1.2, 2.4);
编译器看到这一行,就会自动帮我们生成double版本的max函数,生成出来的函数等价于把函数模板中的所有T都替换成double。在这里max<double>可以看做是double版本的max函数的函数名,我们甚至还可以用&max<double>来获取这个函数的地址。
我们来看一个更复杂的例子
template<typename T, int i> T create() { T value(); return value + i; } int main() { float f1 = create<float, 1>(); // f1 == 1.0 float f2 = create<float, 2>(); // f2 == 2.0 }
这个例子里面我们定义了一个create函数模板,根据模板创建并使用了两个函数create<float, 1>和create<float, 2>。要注意的是,这两个函数是不同的函数,有不同的函数体,和不同的函数地址。他们两个分别等价于
float create() // create<float, 1> { float value(); return value + 1; } float create() // create<float, 2> { float value(); return value + 2; }
我们总结一下函数模板的语法,模板定义由template关键字开始,后面跟着一对尖括号,尖括号里面是模板形参列表,也就是模板的参数。模板形参列表的写法和函数形参列表的写法是很相似的。都是“类型 参数名, 类型 参数名, ...”这种形式。上面的例子中,模板形参列表就是“typename T, int i”。我们注意到,模板形参列表需要为每个形参指定一个类型,这个是因为形参不一定是C++类型,还可以是具体的值,例如数字,指针等等。如果形参是一个类型,则需要使用typename关键字来表示形参的类型,如果形参是一个值,则需要写上这个值的类型。在使用模板的时候,要在模板的名字后面加一对尖括号,尖括号里面是模板实参列表,在上面的例子中,实参列表就是“float, 1”和“float, 2”。与函数调用类似,使用模板的时候编译器会检查实参列表的类型与形参列表的类型是否匹配,不匹配的话会报错。
使用函数模板的优点
我们可以从上面的例子中看出,用函数模板更方便简洁,不需要重复写类似的重载函数。除此外,因为函数代码是在使用的时候生成出来的,所以如果我们没有使用这个函数,编译器就不会生成这个代码,这样我们可以减小程序的大小。例如,我们使用了max<double>,但是没有使用max<int>,那么程序中只有max<double>函数,不会有max<int>函数。