条款1 :理解模板型别推导
如果一个复杂系统的用户对于该系统的运作方式一无所知,然而却对其提供的服务表示满意,这就充分说明系统设计得好。如果从这样的角度来看, C++的模板型别推导取得了极大的成功。
模板的型别推导,是现代C++最广泛应用的特性之一 ———— auto的基础。也就是说,如果你对C++98 推导模板型别的运作方式感觉满意,那么你也会自然而然地愉快接受C++11 中推导 auto 型别的运作方式。而坏消息则是,当模板型别推导规则应用于auto语境时,它们不像应用于模板时那么符合直觉。出于这个缘故,了解作为auto 基础的模板型别推导的方方面面就变得相当重要了。
不过在学习类型推导之前, 我们有必要复习一下const的基础用法
const 的基本用法
最简单的形式是定义一个常量。一旦初始化,它的值就不能再被改变。这个概念很简单,但当 const 和指针、引用以及模板结合时,情况就变得复杂了。
1 | const int MAX_SIZE = 100; |
const与指针
const 与指针的结合有两种关键形式,理解它们的区别是核心。我们可以通过 const 的位置来判断它的作用对象(距离最近的或者左侧的那个)。
- 首先是指向常量的指针,也称为“底层 const” (low-level const)。
1 | const int* ptr; // 距离最近的是int, 所以const修饰的是int |
含义:你不能通过这个指针 ptr 来修改它所指向的数据。但是,你可以让这个指针 ptr 指向别处。
- 常量指针 (Constant Pointer), 也称为“顶层 const” (top-level const)。
1
2
3
4
5
6
7
8
9
10int* const ptr = &a; // 必须在声明时初始化; 左侧是int*, 所以const修饰的是int*
int a = 10;
int b = 20;
int* const ptr = &a;
*ptr = 15; // 正确!可以通过 ptr 修改 a 的值,现在 a 变成 15
// ptr = &b; // 编译错误!不能修改 ptr 指向的地址
含义:指针 ptr 本身的值(即它存储的地址)是不能被修改的。它必须永远指向初始化时的那个地址。但是,你可以通过它来修改它所指向的数据(前提是数据本身不是 const)。
此外, 指向常量的常量指针 (Constant Pointer to Constant Data)是上面两种情况的结合。指针 ptr 本身的指向和它指向的数据都不能被修改。
1 | const int* const ptr = &a; |
const 与引用
const 与引用的关系比较简单,因为引用本身就像一个别名,它不能被“重新绑定”到另一个对象,所以只有类似“指向常量的指针”也就是底层调用这一种情况。
1 | int a = 10; |
const int& 是一个非常重要的编程惯用法。当按引用传递函数参数时,如果函数不需要修改这个参数,强烈建议形参使用 const 引用。这有两个好处:首先是安全, 防止函数内部意外修改了外部数据。其次还高效:避免了按值传递时创建对象的拷贝开销。同时也具有灵活性, 可以接受临时对象(右值)。
模板型别推导
所谓 C++ 模板参数推导的核心目标是:编译器必须为模板参数(如 T)找到一个(唯一的)类型,使得当这个 T 被替换回函数签名后,形成的形参(Parameter)类型能够合法地接收(绑定)我们传入的实参(Argument)。
模板函数在推导类型 T 时,参数的 const 属性是否被保留,取决于函数参数 param 的声明方式。下列讨论针对的一般形式如下
1 | template<typename T> |
情况 1:按值传递 (T param)
首先, 将实参的引用部分忽略.
接着, 当函数参数是按值传递时,实参的 const 属性会被忽略。这是因为按值传递会创建一个新的对象,而新对象的 const 属性与实参无关。
若实参是个volatile 对象,同忽略之(volatile 对象不常用,它们一般仅用于实现设备驱动程序)。
1 | template<typename T> |
考虑这种情况: expr是个指涉到 const 对象的 const 指针,且 expr按值传给 param:
1 | template<typename T> |
情况 2:按指针或引用传递 (T& 或 T*)
当函数参数是指针或引用时,引用实参的 const 属性或者指针所指数据的const属性会被保留并成为类型 T 的一部分
1 | template<typename T> |
在这个过程中,编译器就像在解一个方程. 再例如:
1 | // 模板函数定义 |
下面我们求解 T:根据规则, T 获取实参的底层const属性, 同时因为传入指针而形参不存在指针, 编译器自然假设 T = const Widget*。
接着将 T 替换回形参 P ( const T& )后得到形参类型:param 的类型变为 const (const Widget) &, 这也通常被读作 const Widget const &。(这是一个“对 [指向 const Widget 的 const 指针] 的 const 引用”)
尝试绑定:编译器再次尝试将实参 (A) 绑定到这个新生成的形参 (P) 上:此时实参 (A): const Widget; 形参 (P): const (const Widget) & (即 const Widget* const &)
因为实参类型 const Widget* 与形参期望引用的核心类型的主体 const Widget* 完全匹配。没有发生任何丢弃常量性的危险操作; 同时我们的实参(&vw[0] 的结果)是一个临时产生的指针,它是一个右值 (rvalue)。C++ 规定,右值不能绑定到非 const 的左值引用 (non-const lvalue reference),但它们可以完美地绑定到 const 的左值引用 (const lvalue reference)。我们的形参 const (const Widget*) & 正是一个 const 左值引用,因此它可以合法地绑定来自实参的右值。
情况 3:按万能引用传递 (T&& param)
这种情况更为特殊,但规则与情况2类似:const 属性会被保留。
1 | template<typename T> |
详细情况线上略
数组实参和字符指针
以上已经基本讨论完模板型别推导的主流情况,但还有一个边缘情况值得了解。这种情况是:数组型别有别千指针型别,尽管有时它们看起来可以互换。形成这种假象的主要原因是,在很多语境下,数组会退化成指涉到其首元素的指针。
函数实参和函数指针
总之, 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用