EMCPP条款1:理解模板型别推导
整理自“Effective Modern C++”.
- 中文翻译不准确的或者不易于理解的地方使用英文原文代替
型别推导的目的是使人们不必再去写下那些不言自明或是完全冗余的型别。它还让C++软件获得更高的适应性,因为在源代码的一个地方对一个型别实施的改动,可以自动通过型别推导传播到其他地方。
用一小段伪代码来说明,函数模板大致形如:
1 | template<typename T> |
T的型别推导结果要分三种情况讨论:
ParamType
具有指针或引用型别,但不是个万能引用(万能引用会在条款24中介绍,现在你只需知道有这么个东西,并且它们与左值引用和右值引用都有所区别即可)。ParamType
是一个万能引用。ParamType
既非指针也非引用。
情况1: ParamType
是个指针或引用,但不是个万能引用
- 若
expr
具有引用型别,先将引用部分忽略。 - 尔后,对
expr
的型别和ParamType
的型别执行模式匹配,来决定T
的型别。
例如,我们的模式如下:
1 | template<typename T> |
又声明了下列变量:
1 | int x = 27; // x 的型别是 int |
在各次调用中,对param
和T
的型别推导结果如下:
1 | f(x); // T 的型别是 int. param 的型别是 int& |
尽量上述调用语句示例演示的都是左值引用形参,但是右值引用形参的型别推导运作方式是完全相同的。当然,传给右值引用形参的,只能是右值引用实参,但这个限制和型别推导无关。
如果我们将形参型别从T&
改为const T&
,结果会有一点变化,但这些变化并没有什么出人意料之处,cx
和rx
的常量性仍然得到了满足:
1 | template<typename T> |
如果param
是个指针(或指涉到const
对象的指针)而非引用,运作方式本质上并无不同:
1 | template<typename T> |
情况2: ParamType
是个万能引用
- 如果
expr
是个左值,T
和ParamType
都会被推导为左值引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T
被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。 - 如果
expr
是个右值,则应用“常规"(即情况1中的)规则。
1 | template<typename T> |
当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用是从来不会作这样的区分的。
情况3: ParamType
既非指针也非引用
当ParamType
既非指针也非引用时 ,我们面对的就是所谓按值传递了:
1 | template<typename T> |
param
会是个全新对象这一事实促成了如何从expr
推导出T
的型别的规则:
- 一如之前,若
expr
具有引用型别,则忽略其引用部分。 - 忽略
expr
的引用性之后,若expr
是个const
对象,也忽略之。若其是个volatile
对象,同忽略之(volatile
对象不常用,它们 一般仅用千实现设备驱动程序。欲知详情,参见条款 40)。仅仅由于1
2
3
4
5
6
7int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both intexpr
不可修改,并不能断定其副本也不可修改。需要重点说明的是,const
(和volatile
)仅会在按值形参处被忽略。正如此前所见,若形参是const
的引用或指针(references-to- or pointers-toconst
),expr
的常量性会在型别推导过程中加以保留。但是,考虑这种情况:expr
是个指涉到const
对象的const
指针,且expr
按值传给param
:左边1
2
3
4
5
6
7template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = "Fun with pointers";
// ptr is const pointer to const object
f(ptr); // pass arg of type const char * constconst
表示ptr
指向的内容不能修改,右侧const
表示ptr
本身的值不能修改。按值传递时,其自身的const
属性被忽略,所以param
型别会被推导为const char*
。The constness of whatptr
points to is preserved during type deduction, but the constness ofptr
itself is ignored when copying it to create the new pointer,param
.
数组实参
首先,在很多语境下,数组会退化成指涉到其首元素的指针:
1 | const char name[] = "J. P. Briggs"; // name 的型别是 c on st char [1 3] |
但当一个数组传递给持有按值形参的模板时,
1 | template<typename T> |
由于数组形参声明会按照它们好像是指针形参那样加以处理,按值传递给函数模板的数组型别将被推导成指针型别。也就是说,在模板 f
的调用中,其型别形参 T
会被推导成 const char*
:
1 | f(name); // name 是个数组,但 T 的型别却披推导成 const char * |
尽管函数无法声明真正的数组型别的形参,它们却能够将形参声明成数组的引用!所以,如果我们修改模板 f
, 指定按引用方式传递其实参,
1 | template<typename T> |
在这种情况下,T
的型别会被推导成实际的数组型别!这个型别中会包含数组尺寸,在本例中,T
的型别推导结果是 const char [13]
,而 f
的形参(该数组的一个引用)型别则被推导为 const char (&)[13]
。
有意思的是,可以利用声明数组引用这一能力创造出一个模板,用来推导出数组含有的元素个数:
1 | // return size of an array as a compile-time constant. (The |
函数实参
函数型别也同样会退化成函数指针,并且我们针对数组型别推导的一切讨论都适用千函数及其向函数指针的退化。所以结果如
下:
1 | void someFunc(int, double); |
要点速记
- 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理。换言之,其引用性会被忽略。
- 对万能引用形参进行推导时,左值实参会进行特殊处理。
- 对按值传递的形参进行推导时,若实参型别中带有
const
或volatile
饰词,则它们还是会被当作不带const
或volatile
饰词的型别来处理。 - 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。