声明:本文翻译自《Effective Modern C++》,自己边看边翻译的,不保证与英文原版完全字字对应,纯粹以学习为目的,请勿转载和用于商业用途。
如果一个复杂系统的用户对系统的工作原理一无所知,但是却能很愉悦地操作它,这说明系统设计的非常好。从这个角度看,C++的模板类型推断是极其成功的。数百万的程序员曾将参数传给模板函数,并且执行结果完全符合预期,但是很多人对于传递给这些函数的参数是如何进行类型推断的几乎一无所知。
如果你属于他们中的医院,我要告诉你一个好消息和一个坏消息。好消息是:现代C++最引人注目的功能之一:auto
,是模板类型推断的基石。如果你对C++98标准的模板类型推断得心应手,那么你将对C++11标准中auto
的类型推断同样信手拈来。坏消息是:当将模板类型推断法则用于auto
上下文时,并没有像用于模板中那么直观。因此,真正理解auto
模板类型推断的本质极其重要。本条款包含你需要知道的内容。
如果你愿意瞄一眼伪代码片段,考虑如下的函数模板:
template<typename T>
void f(ParamType param);
可以这样调用函数:
f(expr); // 调用f
在编译期间,编译器利用expr
推断两种类型:分别是T
和ParamType
。这两种类型通常是不同的,因为ParamType
一般包含修饰符,如const
或者引用限定符。举个例子,如果模板声明如下:
template<typename T>
void f(const T& param); // ParamType is const T&
如果我们这样调用:
int x = 0;
f(x); // 利用int值调用
T
被推断为int
类型,但是ParamType
被推断为const int&
类型。
我们当然期望传递给函数的参数类型和T
的推断类型完全一样,即T
是expr
类型的。对于上面的例子,情况就是这样:x
是int
类型,T
也被推断为int
。但是这并不总是正确的,T
的类型推断并不仅仅依赖于expr
的类型,还跟ParamType
的形式有关,有三种可能的情形:
因而,我们需要开了这三种类型推断。每一种读基于以下的同样模板类型和调用方式:
template<typename T>
void f(ParamType param);
f(expr); // 从expr推断T和ParamType
ParamType
是引用或者指针,但不是万能引用最简单的情形就是ParamType
是引用或者指针类型,但不是万能指针。此时,类型推断的过程如下:
expr
是引用类型,忽略引用;ParamType
匹配,从而推断T例如,模板如下:
template<typename T>
void f(T& param); // param是引用类型
变量声明如下:
int x = 27; // x是int类型
const int cx = x; // cx是const int类型
const int& rx = x; // rx是指向const int的引用类型
以如下不同的调用方式推断param
和T
的类型:
f(x); // T是int类型,而param是int&类型
f(cx); // T是const int类型,param是const int&类型
f(rx); // T是const int类型,param是const int&类型
在第二种和第三种调用方式中,注意到由于cx
和rx
都被赋予const
类型的值,T
被推断为const int
类型,从而参数类型被推断为const int&
。这对于调用者来说很重要,当向引用参数传递const
对象时,该对象仍然是不可修改的,即参数是const
引用类型。这说明了将const
对象传递给带有T&
参数的模板是安全的,因为对象的const
属性是T
推断类型的一部分。
注意到在第三种调用方式中,尽管rx
是引用类型的,但T
被推断为非引用类型,这是因为rx
的引用在类型推断中被忽略了。
以上示例都是针对的左值引用,但对于右值引用参数规则实际上完全一样的。当然,只有右值参数才能传递给右值引用参数,这个限制跟类型推断无关。
我们我能将f的参数类型从T&
修改为const T&
,情况就不太一样了,但并不用太吃惊。cx
和rx
的const
属性仍然保留,但是由于我们假定param
是const
引用,因此const
不再是T
推断类型的一部分:
template<typename T>
void f(const T& param); // param是const引用
int x = 27; // 保持不变
const int cx = x; // 保持不变
const int& rx = x; // 保持不变
f(x); // T是int类型,param是const int&
f(cx); // T是int,param是const int&
f(rx); // T是int类型,param是const int&
如前所述,rx
的引用属性在类型推断中被忽略了。
如果param
是指针(或者指向const
类型的指针)而非引用类型,情况本质上是完全相同:
template<typename T>
void f(T* param); // param为指针类型
int x = 27; // 保持不变
const int *px = &x; // px为指向const int对象x的指针
f(&x); // T为int类型,param为int*
f(px): // T为const int,param为const int*
此时此刻,你也许在不停的摇头、打哈欠,因为C++的类型推断准则对于引用和指针参数是如此的显而易见,这完全就是你期望的类型推断系统啊!
ParamType
是万能引用类型对于万能引用类型的参数,情形就没有那么显而易见了。万能引用跟右值引用的声明方式类似(对于包含类型参数T的函数模板,万能引用的类型声明为T&&
),但是但传递左值时其行为完全不同。条款24有详细的介绍,这里只给个摘要性介绍:
例如:
template<typename T>
void f(T&& param); // param是万能引用
int x = 27; //保持不变
const int cx = x; // 保持不变
const int& rx = x; // 保持不变
f(x); // x为左值,因此T为int&;param也是int&
f(cx); // cx是左值,因此T是const int&;param也是const int&
f(rx); // rx是左值,因此T是const int&;param也是const int&
f(27); // 27是右值,因此T是int;param是int&&类型
条款24确切地解释了为什么是这样的。这里需要关注的是:万能引用参数的类型推断准则和左值及右值引用参数的推断准则不同。特别的,在使用万能引用时,类型推断会区分左值参数和右值参数,但非万能引用则不会。
ParamType
既非指针,也非引用但ParamType既非指针,也非引用时,我们将其视为传值处理:
template<typename T>
void f(T param); // param为传值参数
这意味着param
是传递过来对象的拷贝,一个全新的对象。param
为全新对象的事实形成了如下准则,该准则决定了T
如何从expr
中推断:
expr
为引用类型,则忽略引用;expr
是const
类型的,继续忽略const
属性。如果expr
是volatile
的,也忽略。(volatile
对象并不常见,通常仅用于实现设备驱动,详见条款40)。因此:
int x = 27; // 保持不变
const int cx = x; // 保持不变
const int& rx = x; // 保持不变
f(x); // T和param都是int类型
f(cx); // T和param也都是int类型
f(rx); // T和param还是int类型
注意:尽管cx
和rx
都是const
值,但是param
不是const
的。这是有道理的,因为param
和cx
、rx
是完全独立的对象,它只是cx
、rx
的拷贝。cx
和rx
不能修改并不表示param
也不能修改。这就是expr
的const
属性(以及volatile
)在param
类型推断中被忽略的原因:expr
不能修改并不意味着其拷贝也不能修改。
只有传值传递参数时,const
和volatile
才能被忽略,意识到这一点非常重要。如我们所知,对于const
引用或者指向const
的指针参数,expr
的const
属性在类型推断中仍然保留。但如果expr
是指向const
对象的const
指针,并且expr
通过传值传递给param
,情形如下:
template<typanem T>
void f(T param); // param通过传值传递
const char* const ptr =
"Fun with pointers"; // ptr是指向const对象的const指针
f(ptr); // 传递const char* const类型的参数
这里,星号右侧的const
声明了ptr
为const
指针:即ptr
不能改变指向,也不能被置为null
。(星号左侧的const
表明ptr
指向的对象,是const
的,不能修改)。当ptr
传递给f
时,实际上是将指针按字节拷贝至param
。照此推断,指针ptr
本身是通过传值传递的,根据传值传递的类型推断准则,指针的const
属性被忽略,param
的类型被推断为const char*
,即指向const
字符串的可变指针。当将ptr
拷贝构造新指针param
时,ptr
指针所指对象的const
属性在推断中被保留了,但是指针ptr
自身的const
属性被忽略了。
以上讨论已经涵盖了主流模板类型推断的大部分,但是还有一类情形值得了解一下。尽管有时数组和指针类型可以互相转换,但是他们是不同的类型。产生错误认知的主要原因在于:在很多上线问中,数组退化为指向第一个元素的指针,这种退化使得代码像这样编译:
const char name[] = "J. P. Briggs"; //const char[13]类型
const char * ptrToName = name; // 数组退化为指针
这里,使用name
初始化const char*
指针ptrToName
,为name
是const char[13]
类型。const char*
和const char[13]
并不一样,但是基于数组-指针退化准则,代码能通过编译。
(未完待续)