模板与泛型编程
OOP 能处理类型在程序运行之前都未知的情况,在泛型编程中,在编译时就能获知类型。
[!NOTE] Title
- 函数模板和类模板成员函数的定义通常放在头文件中
- 模板程序应该尽量减少对参数类型的要求,即追求通用性
编写泛型代码的两个重要原则:
- 模板中的函数参数是 const 的引用。保证了函数可以用于不能拷贝的类型
- 函数体条件判断使用
<
可以用标准库自带的函数对象less<Type>
代替。 - 注意模板编程不支持分离式编译,即模板类 / 模板函数的声明与定义应该放在头文件里,否则会在链接时报错;
1 模板参数
通常将模板参数设为 T,也可以用任意名字
模板参数名不可重用
1 | template <typename T, typename T> //错误,重用T |
模板声明
与函数参数相同,声明中的模板参数的名字可以和定义中不同
1 | //3个ca1c都指向相同的函数模板 |
使用类的类型成员
使用一个模板类型参数的类型成员,必须显示告诉编译器该名字是一个类型,使用 tyname 关键字:
1 | template <typename T> |
[!NOTE]
当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class
【C++11】默认模板实参
可以为函数模板和类模板提供默认实参
1 | // compare有一个默认模板实参less<T>和一个默认函数实参F() |
如果一个类模板为其所有模板参数都提供了默认实参,我们使用默认实参时就必须在模板名之后跟一个空的<>
1 | template<class T=int> class Numbers //T默认为int |
2 函数模板
不使用模板:
1 | void Print(int temp) |
使用模板:
格式:
**template<typename T>
**:类型模板参数
**template<unsigned T>
**:非类型模板参数
- 模板参数列表不能为空
- typename 也可以写成 class
- typename 表示类型参数,
- unsigned 表示非类型参数,非类型模板参数的模板实参必须是常量表达式。
1 | template<typename T> |
模板实例化
只有当我们实际调用时,编译器用推断出的模板实参代替对应的模板参数为我们实例化一个特定版本的函数。每一个版本称为一个实例。
当编译器遇到模板定义时,它并不生成代码,只有当我们实例化模板的一个特定版本时,编译器才生成代码。
inline 和 constexpr 的函数模板
inline 或 constexpr 说明符放在函数参数列表之后,返回类型之前
1 | //正确:inline说明符跟在模板参数列表之后 |
3 类模板
[!NOTE] Title
- 一个类模板的每个实例都形成一个单独的类
- 默认情况下,对于一个实例化的类模板,成员只有在使用时才被实例化
- 在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参(简化书写:
Array
等价于Array<N>
)
- 与函数模板不同,编译器不能为类模板推断模板参数类型。我们必须在模板名后的尖括号中提供额外信息,用来代替模板参数的模板实参列表。
- 类模板外定义成员函数要先在模板内声明
- 传多个规则给模板,用逗号隔开就行
1 | //可以传类型,也可以传数字,功能太强大了 |
类模板和友元
1 | template<typename T> class BlobPtr; //前置声明 |
【C++11】新标准中我们可以将模板类型参数声明为友元
1 | template<typename T> class Bar |
将用来实例化 Bar 的类型声明为友元,因此对于每个类型名 Foo
,Foo
将成为 Bar<Foo>
的友元,Sales_data
将成为 Bar<Sales_data>
的友元,以此类推。
【C++11】模板类型别名
不能使用 typedef 为模板类型设置别名
可以使用 using
:
1 | //将twin定义为pair<T,T>的别名 |
类模板 static 成员
1 | template <typename T>class Foo |
4 成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。
成员模板不能是虚函数
普通类的成员模板
1 | //函数对象类,对给定指针执行delete |
类模板的成员模板
1 | template <typename T> class Blob |
在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表。
1 | template <typename T> //类的类型参数 |
实例化类模板的成员模板,必须同时提供类和函数模板的实参。
1 | int ia[] = {0,1,2,3,4,5,6,7,8,9}; |
5 【C++11】extern 显式实例化
大系统中,多个文件中可能实例化相同的模板,有不必要的额外开销。
新标准中,可以通过显式实例化来避免这种开销。
1 | //显式实例化格式: |
1 | //实例化声明与定义 |
[!NOTE] 实例化定义会实例化所有成员
实例化定义会实例化所有成员,包括内联的成员函数。因此显式实例化一个类模板的类型时,所用类型必须能用于模板的所有成员函数。
当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明(定义)。
对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern 声明必须出现在任何使用此实例化版本的代码之前:
1 | //Application.cc |
6 模板实参推断
对于函数模板,编译器利用调用中的函数来确定其模板参数。从函数实参来确定模板实参的过程称为模板实参推断。
模板类型参数的类型转换
[!NOTE] Title
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有 const 转换及数组或函数到指针的转换。
如果一个函数形参的类型使用了模板类型参数,那么它和使用普通类型参数初始化规则是不同的。调用时通常不会直接进行类型转换(比如将 T 转换为 int),而是如前文所说,编译器用推断出的模板实参代替对应的模板参数为我们实例化一个特定版本的函数。
如果混用了模板类型参数和普通类型参数,则对普通类型形参对应的实参进行正常的类型转换。模板类型参数则执行上述实例化规则。
模板类型参数有三种情况会直接进行类型转换:
- const 转换:可以将一个非 const 对象的引用(或指针)传递给一个 const 的引用(或指针)形参。
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
- 显式指定的实参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24template<typename T> T fobj(T,T); //实参被拷贝
template <typename T> T fref(const T&,const T&); //引用
string s1("a value");
const string s2("another value");
fobj(s1,s2); //调用fobj(string,string); 顶层const被忽略
fref(s1,s2); //调用fref(const string&,const string&)
//将s1转换为const是允许的
int a[10], b[42];
fobj(a,b); //调用f(int*,int*)
fref(a,b); //错误:数组类型不匹配
//对于模板参数类型已经显式指定了的函数实参,也进行正常的类型转换:
template<typename T> int compare(const T &v1,const T &v2)
{
if(v1<v2) return -1;
if(v2<v1) return 1;
return 0;
}
long lng;
compare(1ng,1024); //错误:模板参数不匹配
compare<long>(1ng,1024); //正确:1024是int转换为long,实例化compare(1ong,long)
compare<int>(1ng,1024); //正确:lng是long转换为int,实例化compare(int,int)
使用相同模板参数类型的函数形参
传递给想给模板参数类型的实参必须具有相同的类型
1 | template<typename T> int compare(const T &v1,const T &v2) |
想使用不同类型就要将函数模板定义为两个类型参数
1 | template<typename A, typename B> |
函数模板显式实参
当函数返回类型与参数列表中任何类型都不相同时,编译器无法推断出模板实参的类型
指定显示模板实参
1 | template <typename T1,typename T2,typename T3> |
提供显示模板实参的方式和定义类模板实例的方式相同。显示模板实参在尖括号中给出,位于函数名之后,实参列表之前:
1 | //T1是显式指定的,T2和T3是从函数实参类型推断而来的 |
尾置返回类型与类型转换
1 | // 尾置返回允许我们在参数列表之后声明返回类型 |
此例中我们通知编译器 fcn 的返回类型与解引用 beg 参数的结果类型相同。解引用运算符返回一个左值(参见 4.l.1 节,第 121 页),因此通过 decltype 推断的类型为 beg 表示的元素的类型的引用。
因此,如果对一个 string 序列调用 fcn, 返回类型将是 string&。如果是 int 序列,则返回类型是 int&。
标准类型转换模板 P606
函数指针的实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
1 | template<typename T> int compare (const T&,const T&); |
显式模板实参消除 func 调用歧义:
1 | //func的重载版本;每个版本接受一个不同的函数指针类型 |
引用的实参推断
1 | template <typename T> void f(T &p); //底层const |
从左值引用函数参数推断类型
当函数参数是模板类型参数是普通(左值)引用 T&时:
1 | template<typename T>void f1(T&); //实参必须是一个左值 |
当函数参数是模板类型参数是 const T&时:
1 | template<typename T> void f2(const T&); //可以接受一个右值 |
从右值引用函数参数推断类型
当函数参数是一个右值引用 T&&时
1 | template <typename T> void f3(T&&); |
7 完美转发与万能引用
概念
转发:函数将一个或多个实参传递给其他函数
完美转发:指转发过程中,保持被转发实参的所有性质,包括实参类型是否是 const 以及实参是左值还是右值。
完美转发基于万能引用,引用折叠以及 std::forward
模板函数。
&&
的含义
**&&
声明不一定是右值引用,还可能是万能引用(万能引用经过类型推导和引用折叠可能是左值引用也可能是右值引用)
1 | Widget&& var1 = someWidget; // 右值引用 |
绑定规则:
- 左值引用
&
: 可以绑定到左值上,还可以绑定到右值上: const 左值引用可以绑定到左值和右值,非 const 左值引用只能绑定到左值 - 右值引用
&&
: 只能绑定到右值上 - 万能引用
T&&
:可以绑定到左值上或者右值上
1 | 左值引用 {左值} |
1 | const string &s = "asd"; // ok, const 左值引用绑定到右值 |
万能引用
如果一个变量或者参数被声明为 T&&
,其中 T
是被推导的类型 (T
可以是模板参数或 auto
),那这个变量或者参数就是一个万能引用 universal reference。
引用折叠只会在一些特定的可能会产生 “引用的引用” 场景下生效。这些场景包括模板类型推导,
auto
类型推导,typedef
的形成和使用,以及decltype
表达式。本问只讨论模板类型推导和 auto 类型推导
和所有的引用一样,你必须对 universal references 进行初始化,而且正是 universal reference 的 initializer 决定了它到底代表的是 lvalue reference 还是 rvalue reference:
- 如果用来初始化 universal reference 的表达式是一个左值,那么 universal reference 就变成 lvalue reference。
- 如果用来初始化 universal reference 的表达式是一个右值,那么 universal reference 就变成 rvalue reference。
发生这种现象的原因就是万能引用+引用折叠
例如:
1 | template<typename T> |
转换细节下文会提到:
万能引用模板类型推导
当我们向万能引用类型模板参数传参时,模板类型推导遵循以下规则:
- 类型为
T
的左值被推导为T&
- 类型为
T
的右值被推导为T
1 | template<typename T> |
引用折叠
C++ 不允许出现引用的引用。如果代码当中显式的定义了一个引用的引用,那代码就是不合法的。
如节代码所述,万能引用经过类型推导会间接创建引用的引用,为了防止编译器报错。C++11 引入了一个叫做 “引用折叠” 的规则来处理某些像模板实例化这种情况下带来的 “引用的引用” 的问题。
通常我们不能直接定义一个引用的引用,但是通过类型别名或通过模板类型参数间接创建是可以的,间接创建引用的引用会发生“引用折叠”。
“引用的引用” 就有四种可能的组合: T& &
、T& &&
、T&& &
、T&& &&
- @ 引用折叠规则:
- 右值引用的右值引用折叠成右值引用类型:
T&& &&
折叠成T&&
. - 所有其他种类的引用的引用都折叠为左值引用:
T& &
、T& &&
和T&& &
都折叠成T&
- 右值引用的右值引用折叠成右值引用类型:
我们来看下分别用 rvalue 和 lvalue 来调用一个接受 universal reference 的模板函数时会发生什么:
1 | template <typename T> |
当 Test 函数传入左值 str 时,T
推导为左值引用 string&
,此时形参 s 的类型为 string& &&
。经过引用折叠,折叠为 string&
, 所以模板函数被推断为 void Test (string& s)
当 Test 函数传入右值 str 时,T
推导为原类型 string
,此时形参 s 的类型为 string&&
,没有触发引用折叠,所以模板函数被推断为 void Test (string&& s)
std:: move 和 std::forward
std::move
显式地将一个左值对象转换为右值引用std:: move
解决的问题是对于一个本身是左值的右值引用变量需要绑定到一个右值上
std::forward<T>
必须通过显式模板实参来调用,返回该显式实参类型的右值引用,**即std::forward<T>
返回类型是T&&
**(这是一个万能引用,通过万能引用模板推导和引用折叠,就可以实现传左值将该万能引用转换为左值引用,传右值将该万能引用返回右值引用)std:: forward<T>
解决的问题是一个绑定到 universal reference 上的对象可能具有 lvalueness 或者 rvalueness,正是因为有这种二义性,所以催生了std::forward<T>
: 如果一个本身是左值的万能引用如果绑定在了一个右值上面,就把它重新转换为左值。函数的名字 (“forward
”) 的意思就是。**我们希望在传递参数的时候,可以保存参数原来的左右值性,实现完美转发
《Effective Modern C++》中建议:对于右值引用使用
std::move
,对于万能引用使用std::forward<T>
。std::move()
与std::forward<T>
都仅仅做了类型转换而已。真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。std::move
与std::forward<T>
本质都是static_cast
转换
完美转发
完美转发 = 万能引用 + 引用折叠 + std::forward
我们为什么需要完美转发?
我们从一个简单的例子出发。假设有这么一种情况,用户一般使用 testForward 函数,testForward 什么也不做,只是简单的转调用到 print 函数。
1 | template<typename T> |
用户希望 testForward 实参为左值时能调用左值版本的 print void print(T& t)
。实参为右值时能调用右值版本的 print void print(T&& t)
。
注意:在 testForward 中,虽然参数 v 是右值类型的,但此时 v 在内存中已经有了位置,所以在函数内部 v 其实是个左值!
显然,print(v)
和 print(std::move(v))
都不能单独实现用户需求。本质问题在于,左值右值传入 testForward 后,都转化成了左值,print 无法判断 v 原来的左右值性。
print(v)
无论 testForward 传入左值还是右值,永远调用左值版本的 printprint(std::move(v))
无论 testForward 传入左值还是右值,永远调用右值版本的 print
通过完美转发机制可以实现用户需求:通过 std::forward
实现完美转发,使得 print 可以判断 v 原来的左右值性。
- **
print(std::forward<T>(v));
如果 testForward 传入左值,就调用左值版本的 print。如果 testForward 传入右值就调用右值版本的 print。
接下来完美转发解析实现细节:
testForward (x)
回到上面的例子。先考虑 testForward(x);
这一行代码。
模板类型推导+引用折叠
根据万能引用模板类型推导规则,传入的参数 x 是 int
类型的左值,被推导为 int&
,实例化的 testForward 大概长这样:
1 | T = int& |
又根据引用折叠,int& &&
折叠为 int&
:
1 | T = int & |
完美转发
接下来我们来看下 std::forward
在 libstdc++ 中的实现:
1 | template<typename _Tp> |
testForward 中我们调用 std::forward<int &>
,所以此处我们代入 _Tp = int &
,我们有:
1 | constexpr int & && //折叠 |
这里又发生了引用折叠,所以上面的代码等价于:
1 | constexpr int & //折叠 |
在 testForward 函数体内,v 是左值。所以最终 std::forward<int &>(v)
的作用就是将参数强制转型成 int &
,而 int &
为左值。所以,调用左值版本的 print。
即实现传入的参数是左值=>调用左值版本的 print
testForward (std:: move (x))
接下来,考虑 testForward(std::move(x))
的情况。
模板类型推导+引用折叠
根据万能引用模板类型推导规则,传入的参数 std::move(x)
是 int
类型的右值,被推导为 int
,不会发生引用重叠。实例化的 testForward 大概长这样:
1 | T = int |
完美转发
std::forward
在 libstdc++ 中的实现:
1 | template<typename _Tp> |
testForward 中我们调用 std::forward<int>
,所以此处我们代入 _Tp = int
,我们有:
1 | constexpr int && |
没有发生引用折叠,上面的代码等价于:
1 | constexpr int && |
在 testForward 函数体内,v 是左值。所以最终 std::forward<int>(v)
的作用就是将参数强制转型成 int&&
,而 int&&
为右值。所以,调用右值版本的 print。
即实现传入的参数是右值=>调用右值版本的 print
理解左值与右值
左右值
精简版定义
对左值和右值进行精确定义是一件很难的事 (c++11 标准基本上是通过举例来说明一个表达式是否是一个 lvalue 还是 rvalue 的)。
但实践当中,下面的定义就足够了:
- 如果你可以对一个表达式取地址,那这个表达式就是个 lvalue。
- 如果一个表达式的类型是一个 lvalue reference (例如,
T&
或const T&
, 等.),那这个表达式就是一个 lvalue。 - 其它情况,这个表达式就是一个 rvalue。从概念上来讲 (通常实际上也是这样),rvalue 对应于临时对象,例如函数返回值或者通过隐式类型转换得到的对象,大部分字面值 (例如 10,3.14,”hello”) 也是 rvalues。
对于判断一个表达式是否是左值的一个有用的启发就是,看看能否取得它的地址。如果能取地址,那么通常就是左值。如果不能,则通常是右值。
完整版定义 (了解即可)
实际上,上述不太完整,标准里的定义实际更复杂,规定了下面这些值类别(value categories):
一个 lvalue 是通常可以放在等号左边的表达式,
一个 rvalue 是通常只能放在等号右边的表达式。
一个 glvalue 是 generalized lvalue(广义左值),
一个 xvalue 是 expiring lvalue(即将消亡的左值),
一个 prvalue 是 pure rvalue(纯右值)
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
- 变量、函数或数据成员
- 返回左值引用的表达式,如 ++x、x = 1、cout << ‘ ‘
1 | int&& f(){ |
- 字符串字面量是左值,而且是不可被更改的左值。字符串字面量并不具名,但是可以用 & 取地址所以也是左值。
如 “hello”, 在 c++ 中是 char const [6] 类型,而在 c 中是 char [6] 类型
1 | int main() |
- 如果一个表达式的类型是一个 lvalue reference (例如,
T&
或const T&
, 等.),那这个表达式就是一个 lvalue。
纯右值 (prvalue)
反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为 “临时对象”。最常见的情况有:
- 返回非引用类型的表达式:如 x++、x + 1
- 除字符串字面量之外的字面量:如 42、true
即将消亡的左值(expiring lvalue)
- 隐式或显式调用函数的结果,该函数的返回类型是对所返回对象类型的右值引用
1 | struct As |
对对象类型右值引用的转换
1
2
3
4
5
6
7_有标识符_ _无标识符号_
/ X \
/ / \ \
| l | x | pr |
\ \ / /
\_________X________/
gl r类成员访问表达式,指定非引用类型的非静态数据成员,其中对象表达式是 xvalue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using namespace std;
class shape {
public:
shape() { cout << "shape" << endl; }
virtual ~shape() {
cout << "~shape" << endl;
}
};
class circle : public shape {
public:
circle() { cout << "circle" << endl; }
~circle() {
cout << "~circle" << endl;
}
};
class triangle : public shape {
public:
triangle() { cout << "triangle" << endl; }
~triangle() {
cout << "~triangle" << endl;
}
};
class rectangle : public shape {
public:
rectangle() { cout << "rectangle" << endl; }
~rectangle() {
cout << "~rectangle" << endl;
}
};
class result {
public:
result() { puts("result()"); }
~result() { puts("~result()"); }
};
result process_shape(const shape &shape1, const shape &shape2) {
puts("process_shape()");
return result();
}
int main() {
process_shape(circle(), triangle());
}
xvalue 有标识符,所以也被称为 lvalue。跟左值 lvalue 不同,xvalue 仍然是不能取地址的——这点上,xvalue 和 prvalue 相同。所以,xvalue 和 prvalue 都被归为右值 rvalue。如下所示:
1 | shape |
生命周期延长
一个变量的生命周期在超出作用域时结束。如果一个变量代表一个对象,当然这个对象的生命周期也在那时结束。临时对象生命周期 C++ 的规则是:一个临时对象会在包含这个临时对象的完整表达式估值完成后、按生成顺序的逆序被销毁,除非有生命周期延长发生。
为了方便对临时对象的使用,C++ 对临时对象有特殊的生命周期延长规则。这条规则是:如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。
需要万分注意的是,这条生命期延长规则只对 prvalue 有效,而对 xvalue 无效。 如果由于某种原因,prvalue 在绑定到引用以前已经变成了 xvalue,那生命期就不会延长。不注意这点的话,代码就可能会产生隐秘的 bug。
生命周期延长应用
生命周期延长可以被应用在析构函数上,当我们想要去继承某个基类的时候,这个基类往往会被声明为 virtual
,当不声明的话,子类便不会得到析构。如果想让这个子类对象的析构仍然是完全正常,你可以把一个没有虚析构函数的子类对象绑定到基类的引用变量上。
例如:
1 | template<typename T> |
输出:
1 | f(10); // 10 is an rvalue |
大家可以发现,当把子类绑定到基类的时候,子类析构正常了,这便是生命周期延长的应用。
表达式的左右值性与类型无关
- 左右值性:左值还是右值
- 值类型:是相对于引用类型而言的
- 值类型:即 int 类型,float 类型,枚举类型,类类型,结构体类型等。
- 引用类型:引用、指针
一个表达式的左值性)或者右值性和它的类型无关。
也就是说,有个类型 T
,你可以有类型 T
的左值和右值。
以 int
为例,可以有 lvalue 的 int
(例如声明为 int 的变量),还有 rvalue 的 int
(例如字面值 10
)。
当你碰到右值引用类型的形参时,记住这一点非常重要,因为形参本身是个左值:
1 | class Widget { |
在这里,在 Widget
移动构造函数里取 rhs
的地址非常合理,所以 rhs
是左值,尽管它的类型是右值引用。(由于相似的原因,所有形参都是左值。)
8 模板重载
详细 P614
1 | //名字相同的函数必须具有不同数量或类型的参数 |
如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本
9 【C++11】可变参数模板
#可变参数模板
可变参数模板接受可变数目的参数
可变数目的参数被称为参数包,存在两种参数包:
- 模板参数包:表示 0 个或多个模板参数
- 函数参数包:表示 0 个或多个函数参数
- 用一个省略号
...
来指出一个模板参数或函数参数表示一个包。
在一个模板参数列表中,class…
或 typename...
指出接下来的参数表示零个或多个类型的列表;
一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。
在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
1 | //Args是一个模板参数包;rest是一个函数参数包 |
sizeof… 运算符
当我们需要知道包中有多少元素时,可以使用 sizeof.…运算符。类似 sizeof,sizeof… 也返回一个常量表达式,而且不会对其实参求值:
1 | template<typename ... Args> void g(Args ... args) |
包扩展
扩展一个包时,我们要提供用于每个扩展元素的模式。
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
1 | template <typename T,typename...Args> |
扩展 Args:为 print 生成函数参数列表。编译器将模式 const Args&引用到模板参数包 Args 中的每个元素。此模式的扩展结果为 0 个或多个 const type&类型的列表。
1 | print(cout,i,s,42); //包中有两个参数:s和42 |
扩展 rest:为 print 调用生成参数列表。在此情况下,模式是函数参数包的名字(即 rest)。此模式扩展出一个由包中元素组成的的列表。因此,这个调用等价于:
1 | print (os,s,42); |
转发参数包
可变参数函数通常将它们的参数转发给其他函数。
1 | // fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用 |
这里我们希望将 fun 的所有实参转发给另一个名为 work 的函数,假定由它完成函数的实际工作。work 调用中的扩展既扩展了模板参数包也扩展了函数参数包。
由于 fun 的参数是右值引用,因此我们可以传递给它任意类型的实参;由于我们使用 std:: forward 传递这些实参,因此它们的所有类型信息在调用 work 时都会得到保持。
10 模板特例化
一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字 template 后跟一个**空尖括号对 (< >
)**。空尖括号指出我们将为原模板的所有模板参数提供实参。
理解此特例化版本的困难之处是函数参数类型。当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。本例中我们特例化:
1 | template <typename T> int compare(const T&,const T&); |
我们希望定义此函数的一个特例化版本,其中 T 为 const char。我们的函数要求一个指向此类型 const 版本的引用。我们需要在特例化版本中使用的类型是 const char const&, 即一个指向 const char 的 const 指针的引用。
1 | //compare的特殊版本,处理字符数组的指针 |
[!NOTE]
- 特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
- 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
- 类模板可以部分特例化,可以只指定一部分而非所有模板参数。还可以只特例化成员而不是类。
模板特化的目的
模板本来是一组通用逻辑的实现,但是可能存在特定的参数类型下,通用的逻辑实现不能满足要求,这时就需要针对这些特殊的类型,而实现一个特例模板—即模板特化。
重点注意
- 类模板:支持全特化、偏特化
- 类模板调用优先级:全特化类 > 偏特化类 > 主版本模板类;
- **函数模板:支持全特化,支持函数模板重载。函数模板是不允许偏特化的,但可以通过函数模板重载,从而声明另一个函数模板即可替代偏特化的需要。
- 函数模板同时存在具体化模板、函数模板重载、和常规函数重载时候,调用优先级:常规函数 > 具体化模板函数 > 常规模板函数;
- 注意:重载决议时,优先决议出是不是符合常规函数,不存在符合的普通函数,才会再决议出符合的函数主模板,对于函数模板重载决议,会无视特化存在 (标准规定:重载决议无视模板特化,重载决议发生在主模板之间), 决议出函数主模板后,如果函数主模板存在符合的具体化函数模板,才会调用具体化函数模板;
- 不能将函数模板特化和重载混为一谈。函数特化都没有引入一个全新的模板或者模板实例,它们只是对原来的主(或者非特化)模板中已经隐式声明的实例提供另一种定义。在概念上,这是一个相对比较重要的现象,也是特化区别于重载模板的关键之处。
- 如果使用普通重载函数,那么不管是否发生实际的函数调用,都会在目标文件中生成该函数的二进制代码。而如果使用函数模板特化版本,除非发生函数调用,否则不会在目标文件中包含特化模板函数的二进制代码。这符合函数模板的 “惰性实例化” 准则。
函数模板的特化
1)常规函数
1 | // 常规函数 |
2)函数主模板 a
1 | //函数主模板 |
3)函数模本 b– 全特化
针对 char * 类型的比较函数,不能直接使用大于号或小于号比较元素,需要特化;
1 | //函数模板-全特化 |
4)函数模板 c - 重载,作为一个独立的函数主模板
1 | //函数模板-重载,不是偏特化,它会作为一个独立的函数主模板 |
5)函数模板之间的重载决议
- 当代码中存在如下顺序的申明时
1 | template<typename T, typename N> void Compare(T first, N second) // 函数主模板 a |
那么,发生如下函数调用时,
1 | Compare("1", "2"); |
会打印输出:
这里显示实际调用了函数主模板 c,因为在调用 Compare (“1”, “2”) 时,先会进行重载决议,发生重载决议,会无视特化存在 (标准规定:重载决议无视模板特化,重载决议发生在主模板之间),那么就会决议出函数主模板 c;
- 当代码中存在如下顺序的申明时,那么,发生如下函数调用时,
1
2
3
4
5
6
7
8template<typename T, typename N> void Compare(T first, N second) // 函数主模板 a
{.....}
template<typename T, typename N> void Compare(T* first, N* second) // 函数主模板 b,是函数主模板 a 的重载
{.....}
template<> void Compare(const char* first, const char* second) // 此时是函数主模板 b 的全特化模板 , 为全特化函数模板 c
{.....}
1 | Compare("1", "2"); |
会打印输出:
这里显示实际调用了全特化函数模板 c,
因为在调用 Compare (“1”, “2”) 时,先会进行重载决议,
发生重载决议,会无视特化存在 (标准规定:重载决议无视模板特化,重载决议发生在主模板之间),
那么就会决议出函数主模板 b, 在判断函数主模板存在符合类型条件的全特化模板 c
6) 常规函数与函数模板之间的重载决议
当代码中存在如下顺序的申明时,
template<typename T, typename N> void Compare (T first, N second) // 函数主模板 a
{…..}
template<> void Compare (const char* first, const char* second) // 函数主模板 a 的全特化模板 b
{…..}
template<typename T, typename N> void Compare (T* first, N* second) // 函数主模板 c,是函数主模板 a 的重载
{…..}
void Compare (const char* first, const char* second) // 常规函数
{…..}
那么,发生如下函数调用时,
Compare (“1”, “2”);
会打印输出:
这里显示实际调用了常规函数,
因为在调用 Compare (“1”, “2”) 时,先会进行重载决议,重载决议会优先决议是否存在符合条件的常规函数。
7) 函数模板全部实现
1 | /******************************* template function start ****************************************/ |
打印输出如下图,
类模板的特化
必须先有泛化版本类模板(主模板),才有特化版本类模板。
1)类模板特化分类
特化为绝对类型(全特化);
特化为引用,指针类型 (半特化、偏特化);
特化为另外一个类模板 (复杂点的偏特化)
2)类模板 - 主模板类
1 | //类模板-主版本模板类 |
3)类模板 - 全特化(具体化)
1 | //类模板-全特化(具体化) |
4)类模板 - 偏特化(部分具体化), 对部分模板参数进行特化为一般类型
1 | //类模板-特化(部分具体化),对部分模板参数进行特化 |
5)类模板 - 偏特化(部分具体化), 将模板参数特化为指针
1 | //类模板-特化(部分具体化),将模板参数特化为指针 |
6)类模板 - 偏特化(部分具体化), 将模板参数特化为另一个模板类
1 | //类模板-特化(部分具体化),将模板参数特化为另一个模板类 |
7)类模板特化全部实现
1 | /******************************* template class start ****************************************/ |
执行后打印输出如下图所示,
可以看到 MyClass<int, int> 类型声明的对象,函数 Compare 地址是一样的。