[译] Item 1: Understand Template Type Deduction
在使用一个复杂的系统时,我们可以不用知道具体的细节。从这个方面来说,C++ 的模板类型推导是很成功的。数以百万的程序员都使用过模板,即使他们可能很难描述清楚这些类型是如何推导的。
如果你是其中的一员,我有一个好消息,也有一个坏消息。好消息是,模板类型推导是最引人注目的 C++11 新特性 auto
的基础。如果你很清楚 C++98 中的模板类型推导,那么你会很容易明白 C++11 中的 auto
。坏消息是,当使用 auto
时,有些类型推导会变得没有那么直观。因此完全掌握类型推导的规则是非常有必要的。Item 1 会介绍你必须知道的类型推导规则。
我们从一小段伪代码开始:
1 2 |
|
调用如下:
1
|
|
在编译期间,编译器使用表达式 expr
来推导两个类型:一个是 T
另一个是 ParamType
。这两个类型通常是不一样的,因为 ParamType
通常是含有修饰的,比如 const
或者引用。举个例子,如果模板的声明如下:
1 2 |
|
然后调用如下:
1 2 |
|
类型 T
会被推导为 int
,而 ParamType
被推导为 const int&
。
将类型 T
推导为传入的参数类型是很自然的,即 T
的类型就是 expr
的类型。在上面的例子中,x
是 int
类型,T
被推导为 int
类型。但是有时候却不是这样的。类型推导不仅仅依赖于表达式 expr
的类型,同时还依赖于 ParamType
的形式。有下面三种情况:
- ParamType 是一个指针或者引用类型,但是不是全局引用。(全局引用将会在 Item 24 中讲述。现在你只需要知道他不同于引用,也不同于右值引用即可)。
- ParamType 是一个全局引用。
- ParamType 即不是指针也不是引用
因此我们需要考虑上面描述的三种场景来一一讲述。每个都基于下面的伪代码,
1 2 3 4 |
|
情况1:ParamType
是一个指针或者引用,但不是右值引用
这是最简单的一种情况,在这种情况下,类型推导的工作如下:
- 如果
expr
是一个引用类型,忽略引用部分。 - 然后模式匹配
expr
的类型,根据ParamType
的类型来决定T
的类型。
举个例子,如果我们的模板是下面这样,
1 2 |
|
然后我们有如下的变量声明,
1 2 3 |
|
param
和 T
的类型推导结果如下,
1 2 3 4 5 |
|
在第二个以及第三个调用中,注意由于 cx
和 rx
是 const
值,所以 T
被推导为 const int
类型,这对调用者来说是至关重要的。当调用者传入一个 const
对象给一个引用参数时,参数仍然是不可更改的。比如,参数是一个 const
引用。这也是为什么传入一个 const
对象给一个接受 T&
类型的模板参数是安全的:const
约束直接变为了 T
类型的一部分。
在第三个调用中,注意虽然 rx
的类型是引用,T
仍然被推导为非引用。这是由于类型推导过程中 rx
的引用修饰被忽略掉了。
上面的例子都是左值引用参数,但其实右值引用的类型推导和上面是完全一样的。当然,只有右值类型的参数才能传递给右值引用,但他对类型推导完全没有影响。
如果我们将 f
的参数类型由 T&
更改为 const T&
,情况会有一些变化,但非常好理解。cx
和 rx
的 const
修饰仍然有用。但由于现在假定 param
的类型是一个 const
引用,现在不再需要将 const
推断为类型 T
的一部分了:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
和上面一样,rx
的引用修饰在类型推导时会被忽略。
如果 param
是一个指针(或者 const
指针),类型推导基本是一样的。
1 2 3 4 5 6 7 8 9 |
|
到目前位置,你可能已经哈欠不断了,因为对于指针和引用的类型推导,规则这么简单,所有的结果都是显而易见的,就和你想象的完全一样。
情况2:ParamType
是全局引用
当模版参数是全局引用时,类型推导就没有那么明显了。声明的形式有点像右值引用(全局引用的类型声明形式是 T&&
),但是当传入的参数是左值类型时行为是不一样的。具体细节在 Item 24 中讲述,现在我们来看一个简单的版本:
- 如果
expr
是一个左值,那么T
和ParamType
都会推导为左值引用。有两个不寻常的地方。第一,这是仅有的模板类型推导会将类型T
推导为引用类型的情形。第二,尽管ParamType
的类型声明使用了右值引用的符号,但却推导为了左值引用。 - 如果
expr
是一个右值,会使用“正常的”(情况1)规则。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
在 Item 24 中会详细解释为什么在上面的情形中类型推导是这样的。在这里我们的重点是,左值参数和右值参数对于全局引用来说,类型推导的规则是不一样的。这很特别的,当使用全局引用时,类型推导会区别对待左值参数和右值参数,这是在其他类型中不会发生的。
情况3:ParamType
即不是引用也不是指针
当 ParamType
既不是引用也不是指针时,我们来按值传递,
1 2 |
|
这意味着 param
会做一个拷贝 —— 产生一个全新的对象。param
的类型推导遵循如下规则:
- 像上面一样,如果
expr
的类型是一个引用,那么引用类型会被忽略。 - 如果忽略了
expr
的引用修饰之后,expr
是一个const
,那么将const
也忽略。如果有volatile
修饰,也同样忽略。(volatile
修饰并不常用,通常只是在实现设备驱动时会用到。更多细节,参见 Item 40)。
1 2 3 4 5 6 7 |
|
注意,即使 cx
和 rx
都是 const
类型,param
的类型也不是 const
。这是有道理的。param
是一个与 cx
和 rx
无关的对象 —— 是一份拷贝。因此, cx
和 rx
的 const
属性,不会影响 param
。这就是为什么在类型推导过程中 expr
的 const
(以及 volatile)修饰会被忽略的原因:expr
不可更改并不意味着它的拷贝不可以。
意识到 const
(以及volatile
)修饰仅仅在传值参数中会被忽略,是非常重要的。像前面看到的,对于 const
引用,或者 指向 const
对象的指针参数,expr
的 const
修饰是保留到类型推导中的。但是考虑以下情况,expr
是一个指向 const
对象的 const
指针,且 expr
是值传递给 param
的:
1 2 3 4 5 6 |
|
在这里,右边(第二个)的 const
表示 ptr
不可更改。左边(第一个)表示 ptr
指向的常量字符串不可更改。当 ptr
传递给 f
时,ptr
值传递会产生一个拷贝,根据类型推导规则,ptr
的 const
修饰会被忽略掉,最终 param
类型为 const char*
,即指向常量字符串的指针。ptr
所指向的内容的 cosnt
修饰被保留,而 ptr
本身的 const
修饰在值传递时被忽略。
数组参数
几乎所有主流的模板类型推导都包含它,但是还是有一些值得注意的地方。数组类型与指针类型是不同的,尽管他们有时可以转换。在很多时候数组是可以退化成指针的。下面的代码可以编译通过:
1 2 3 |
|
在这里,ptrToName
作为一个 const char*
类型,使用了 const char[13]
类型的 name
来初始化。但是这两个类型是不同的,由于数组到指针的退化规则才使得代码能够编译通过。
但当我们将一个数组类型传递给一个值传递的模版参数时,会发生什么呢?
1 2 3 4 |
|
我们使用没有模板的情况先来观察,对,没错,下面的语法是合法的,
1
|
|
但是数组参数的声明会被退化成指针,这意味着它和下面的声明是完全一致的:
1
|
|
上面的退化是从 C 语言中继承而来的,他给我们造成了数组类型与指针类型是一样的这种幻觉。
由于数组参数的声明会被当作指针来看待,那么当模版参数是数组并且按值传递时,参数会被推导为指针类型。这意味着模板函数 f
的模板参数 T
会被推导为 const char *
:
1
|
|
但是现在问题出现了,尽管参数不能是一个真正的数组类型,但是我们可以声明一个数组的引用。因此当我们更改模板函数 f
使他接受引用参数时,
1 2 |
|
然后我们传入有一个数组,
1
|
|
类型推导会将 T
推导为数组类型,这个类型包括了数组的长度,在这个例子中,T
的类型被推导成为 const char[13]
,函数 f
的参数类型为 const char(&)[13]
。
有趣的是,它提供了一种推导数组元素个数的方式:
1 2 3 4 5 6 7 8 |
|
与 Item 15 中解释的一样,声明一个 constexpr
函数,那么它的返回值在编译期就确定了。这就让用一个已知的数组去声明一个同样大小的数组成为可能:
1 2 3 |
|
当然,作为一个现代的 C++ 程序员,你会很自然的习惯于用 std::array
来代替内建的数组类型,
1
|
|
arraySize
声明时带着 noexcept
,这帮助编译器生成更好的代码。详细内容,参见 Item 14。
函数参数
在 C++ 中,不是只有数组才会退化成指针。函数类型可以退化成函数指针,上面我们讨论的关于数组的类型推导规则也适用于函数,只是退化成函数指针而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
在日常的使用中,这几乎没有什么区别,但是如果你已经知道了数组会退化成指针,那么你也会很自然的知道函数到指针的退化。
这就是模板类型推导的规则。大部分情况下它非常简单。左值在传递给全局引用时的推导有些稍微不同,但是数组和函数类型的退化更让人迷惑。有时候你会想抓住编译器问,“告诉我你在推导什么类型!” 这个时候,你需要 Item 4 中的内容,它会告诉你如何查看正在推导的类型。
需要记住的
- 在模板类型推导时,引用类型的参数会被当作非引用,即引用限定会被忽略。
- 当推导类型是全局引用时,左值会被特殊对待。
- 当推导值传递参数时,
const
,volatile
会被忽略。 - 在类型推导时,数组以及函数名会退化为指针,除非它们被用来初始化引用变量。