引言

模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计。C++ 的标准库提供许多有用的函数大多结合了模板的观念,如STL以及IO Stream。

在这里插入图片描述 在这里插入图片描述

1. 模板

1.1 什么是函数模板

函数模板定义一族函数。

//template1.cpp #include <iostream>

template<typename T> void swap(T &a, T &b) {

T tmp{a}; a = b;

b = tmp;

}

int main(int argc, char* argv[])

{

int a = 2; int b = 3;

swap(a, b); // 使用函数模板

std::cout << "a=" << a << ", b=" << b << std::endl;

double c = 1.1; double d = 2.2; swap(c, d);std::cout << "c=" << c << ", d=" << d << std::endl;return 0;

}

函数模板的格式:

template<parameter-list> function-declaration

函数模板在形式上分为两部分:模板、函数。在函数前面加上 template<...>就成为函数模板,因此对函数的各种修饰(inline、constexpr 等)需要加在 function-declaration 上,而不是 template 前。如

template<typename T> inline T min(const T &, const T &);

parameter-list 是由英文逗号(,)分隔的列表,每项可以是下列之一: 在这里插入图片描述 在这里插入图片描述 上面 swap 函数模板,使用了类型形参。函数模板就像是一种契约,任何满足该契约的类型都可以做为模板实参。而契约就是函数实现中,模板实参需要支持的各种操作。上面

swap 中 T 需要满足的契约为:支持拷贝构造和赋值。

C++ 的函数模板本质上函数的重载,泛型只是简化了程序员的工作,让这些重载通过编译器来完成。我们可以用 c++flit 来观察这一现象。 在这里插入图片描述 再用 f++filt 将 Name Mangling 后的名字翻转过来,就对应了两个函数原型:

void swap<double>(double&, double&)
void swap<int>(int&, int&)

1.2 函数模板不是函数

刚才我们提到函数模板用来定义一族函数,而不是一个函数。C++是一种强类型的语

言,在不知道 T 的具体类型前,无法确定 swap 需要占用的栈大小(参数栈,局部变量), 同时也不知道函数体中 T 的各种操作如何实现,无法生成具体的函数。只有当用具体

类型去替换 T 时,才会生成具体函数,该过程叫做函数模板的实例化。当在 main 函数中调用 swap(a,b)时,编译器推断出此时 T 为 int,然后编译器会生成 int 版的 swap 函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编

写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。函数模板实例化分为隐式实例化和显式实例化。在这里插入图片描述

1.3 隐式实例化 implicit instantiation

仍以 swap 为例,我们在main 中调用 swap(a,b)时,就发生了隐式实例化。当函数模板被调用,且在之前没有显式实例化时,即发生函数模板的隐式实例化。如果模板实参能从调用的语境中推导,则不需要提供。效率较低。

在这里插入图片描述

1.4 显式实例化 explicit instantiation

隐式实例化可能影响效率,所以需要提高效率的显式实例化,显式实例化在编译期间就会生成实例(增加了编译时间)。在函数模板定义后,我们可以通过显式实例化的方式告诉编译器生成指定实参的函数。显式实例化声明会阻止隐式实例化。如果我们在显式实例化时,只指定部分模板实参,则指定顺序必须自左至右依次指定,不能越过前参模板形参,直接指定后面的。 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

1.5. 模板函数的隐式/显示化与特化辨析

如果你真的想要实例化(而不是特殊化或某物)的功能,请执行以下操作:

在这里插入图片描述 总结一下,C++只有模板显式实例化(explicit instantiation),隐式实例化(implicit instantiation),特化(specialization,也译作具体化,偏特化)。首先考虑如下模板函数代码:

在这里插入图片描述 1.隐式实例化

我们知道,模板函数不是真正的函数定义,他只是如其名提供一个模板,模板只有在运行时才会生成相应的实例,隐式实例化就是这种情况:

int main(){ .... swap<int>(a,b); .... } 它会在运行到这里的时候才生成相应的实例,很显然的影响效率这里顺便提一下 swap<int>(a,b);中的<int>是可选的,因为编译器可以根据函数参数类型自动进行判断,也就是说如果编译器不不能自动判断的时候这个就是必要的;

2.显式实例化

前面已经提到隐式实例化可能影响效率,所以需要提高效率的显式实例化,显式实例化在编译期间就会生成实例,方法如下:

template void swap<int>(int &a,int &b); 这样就不会影响运行时的效率,但编译时间随之增加。

3.特化

这个 swap 可以处理一些基本类型如 long int double,但是如果想处理用户自定义的类型就不行了,特化就是为了解决这个问题而出现的:

template <> void swap<job>(job a,job b){...} 其中 job 是用户定义的类型.

2. 函数模板的使用

2.1 使用非类型形参

//template3.cpp #include <iostream> // N 必须是编译时的常量表达式 template<typename T, int N> void printArray(const T (&a)[N]) { std::cout << "["; const char *sep = ""; for (int i = 0; i < N; i++, (sep = ", ")) { std::cout << sep << a[i]; } std::cout << "]" << std::endl; } int main() { // T: int, N: 3 int a[]={1, 2, 3}; printArray(a); float b[] = {1.1, 2.2, 3.3}; printArray(b); return 0; } 在这里插入图片描述

2.2返回值为 auto

有些时候我们会碰到这样一种情况,函数的返回值类型取决于函数参数某种运算后的类型。对于这种情况可以采用 auto 关键字作为返回值占位符。 decltype 操作符用于查询表达式的数据类型,也是 C++11 标准引入的新的运算符,其目的是解决泛型编程中有些类型由模板参数决定,而难以表示的问题。为何要将返回值后置呢?

// 这样是编译不过去的,因为 decltype(a*b)中,a 和 b 还未声明,编译器不知道 a 和 b 是什么。

template<typename T1, typename T2> 
decltype(a*b) multi(T a, T b) {
return a*+ b; 
}
//编译时会产生如下错误:error: use of undeclared identifier 'a' 
//template4.cpp
 #include <iostream>
 using namespace std;
template<typename T1, typename T2>
auto multi(T1 a, T2 b) -> decltype(a * b) {
 return a * b; 
} 
int main(int argc, char* argv[])
{
	cout << multi(2, 3) << endl; 
	cout << multi(2.2, 3.0) << endl; 
	cout << multi(2.2, 4) << endl; 
	cout << multi(3, 4.4) << endl; 
	return 0;
}

在这里插入图片描述

2.3 类成员函数模板

函数模板可以做为类的成员函数。

需要注意的是:函数模板不能用作虚函数。这是因为 C++编译器在解析类的时候就要确定虚函数表(vtable)的大小,如果允许一个虚函数是函数模板,那么就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。

//template5.cpp

#include <iostream>

class object {

public:

template<typename T>

void print(const char *name, const T &v){

std::cout << name << ": " << v << std::endl;

}

};

int main() {

object o;

o.print("name", "Crystal");

o.print("age", 18);

return 0;

}

在这里插入图片描述

2.4 函数模板重载

函数模板之间、普通函数和模板函数之间可以重载。编译器会根据调用时提供的函数参数,调用能够处理这一类型的最佳匹配版本。在匹配度上,一般按照如下顺序考虑:

可以通过空模板实参列表来限定编译器只匹配函数模板,比如 main 函数中的最后一条语句。

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

2.5 函数模板特化 specialization

当函数模板需要对某些类型进行特别处理,这称为函数模板的特化。当我们定义一个特化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。函数模板特化的本质是实例化一个模板,而非重载它。因此,特化不影响编译器函数匹配。

在这里插入图片描述

上面的例子中针对 const char *的特化,我们其实可以通过函数重载达到相同效果。因此对于函数模板特化,目前公认的观点是没什么用,并且最好别用。Why Not Specialize Function Templates?

但函数模板特化和重载在重载决议时有些细微的差别。这些差别中比较有用的一个是阻止某些隐式转换。如当你只有 void foo(int)时,以浮点类型调用会发生隐式转换,这可以通过特化来阻止:

template <class T> void foo(T); template <>
void foo(int) {} foo(3.0); // link error,阻止 float 隐式转换为 int

虽然模板配重载也可以达到同样的效果,但特化版的意图更加明确。

函数模板及其特化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特化版本。

程序运行结果和使用函数模板特化相同。但是,使用普通函数重载和使用模板特化还是有不同之处,主要表现在如下两个方面:

(1)如果使用普通重载函数,那么不管是否发生实际的函数调用,都会在目标文件中生成该函数的二进制代码。而如果使用模板的特化版本,除非发生函数调用,否则不会在目标文件中包含特化模板函数的二进制代码。这符合函数模板的“惰性实例化”准则。

(2)如果使用普通重载函数,那么在分离编译模式下,应该在各个源文件中包含重载函数的申明,否则在某些源文件中就会使用模板函数,而不是重载函数。

2.6 变参函数模板(模板参数包)

这是 C++11 引入的新特性,用来表示任意数量的模板形参。其语法样式如下:

template<typename ...Args> // Args: 模板参数包

void foo(Args ... args); // args: 函数参数包

在模板形参 Args 的左边出现三个英文点号"...",表示 Args 是零个或多个类型的列表,是一个模板参数包(template parameter pack)。正如其名称一样,编译器会将 Args 所表示的类型列表打成一个包,将其当做一个特殊类型处理。相应的函数参数列表中也有一个函数参数包。与普通模板函数一样,编译器从函数的实参推断模板参数类型,与此同时还会推断包中参数的数量。

// sizeof...() 是 C++11 引入的参数包的操作函数,用来取参数的数量

template<typename ...Args>
int length(Args ... args) { return sizeof...(Args); }

// 以下语句将在屏幕打印出:2

std::cout << length(1, "hello") << std::endl;

变参函数模板主要用来处理既不知道要处理的实参的数目也不知道它们的类型时的场景。既然我们对实参数量以及类型都一无所知,那么我们怎么使用它呢?最常用的方法是递归。

//template8.cpp 
#include <iostream> 
using namespace std;
// sizeof...() 是 C++11 引入的参数包的操作函数,用来取参数的数量 
template<typename ...Args> 
int length(Args ... args) {
return sizeof...(Args);
}
int main(int argc, char* argv[])
{ 
// 以下语句将在屏幕打印出:2
cout << length(1, "hello") << endl; // 以下语句将在屏幕打印出:3 
cout << length(1, "hello", 2) << endl;		 // 以下语句将在屏幕打印出:4
cout << length(1, "hello", 2, 3) << endl;	 // 以下语句将在屏幕打印出:5
cout << length(1, "hello", 2, 3, 4) << endl; 
	return 0;
}

已整理到PDF文档, 文章来源:https://www.jianshu.com/p/31d7e18372e2