本文介绍了auto关键字在C++11(及C++14)中的类型推导规则和使用场景。有些地方它不可或缺,但也要避免滥用。
类型推导 (1)
引用和修饰符剥除 (1.1)
在看auto
类型推导之前,先看看模板类型参数的推导:
1 | template<class T> |
在func(cri)
中,类型参数T
最终是什么呢?答案是int
,也就是cri
声明中的const
和&
都被忽略了。要想保留有两个办法:
办法1:
1 | func<const int&>(crx); |
办法2:
1 | template<class T> |
这里,const
和&
(以及&&
)被忽略的现象叫做stripping,这里翻译为剥除吧。
另外,注意下面这种情况,const不会被剥除,因为一旦剥除,const值就变的可修改了:
1 | int j = 20; |
auto推导和上面模板类型参数的推导很相似。对于:
1 | auto a = {expression}; |
- 如果{expression}有引用(无论是左值引用还是右值引用),则引用修饰符(&或者&&)被剥除;
- 如果{expression}有top-level的const/volatile修饰符,则const/volatile被剥除(stripping);
例如:
1 | int i = 0; |
可见,和模板类型参数的推导类似,auto
推导时,&
和&&
是要被剥除的;而const/volatile等关键字,若不影响原变量的属性(例如不会导致const变量可修改),也会被剥除。如果你就想保留引用以及const/volatile属性,怎么办呢?答案是:显式地加上(这也和模板类型参数类似)。由于const和volatile类似,所以只以const为代表,组合左值引用和右值引用,一共会出现4种情况:
- const auto&
- auto&
- const auto&&
- auto&&
我们分别讨论。
const auto& (1.2)
1 | const auto& a = {expression}; |
这种情况比较简单,结果a
的类型就是一个const的左值引用。值得一提的是,const的左值引用,是可以引用右值的,下面r1就是一个例子。
1 | const auto& r1 = 1; |
由于const auto&
声明一个const的左值引用,所以,r1, r2, r3, r4
都是const的,不能修改。
auto& (1.3)
1 | auto& a = {expression}; |
这种情形稍复杂:虽然a
声明中没有const
,但最终a
可能是const的,因为{expression}的const修饰符(若有的话)不能被剥除:
- {expression}的引用可以剥除,但是const(若有的话)不能剥除,因为若剥除const,则常量表达式就能被修改了;
- {expression}只能是左值表达式;最终a是它的左值引用;
举例:
1 | const int c = 2; |
const auto&& (1.4)
1 | const auto&& a = {expression}; |
这种情况也比较简单,结果a
的类型就是一个const的右值引用。
1 | int lv = 3; |
注意,虽然r1的类型是右值引用,但它本身是个左值。这一点在C++11中的右值引用中被反复提到。
auto&& (1.5)
1 | auto&& a = {expression}; |
这种情形比较复杂:
- 首先{expression}的引用被剥除;
- 如果{expression}是
T
类型的左值表达式,则auto
的类型推导结果是T&
,a
的类型是T& &&
,经过引用折叠(reference collapsing)变成T&
;const不可剥除(若剥除,被引用的const变量就能被修改); - 如果{expression}是
T
类型的右值表达式,则auto
的类型推导结果是T
,a
的类型是T&&
;const可剥除(const的右值有意义吗?);
虽然”&&”很容易让人想起右值引用,但它不是!你给它左值,它表现出左值引用的行为;给它右值,它表现出右值引用的行为,这种特殊的引用叫作通用引用。关于通用引用和上面提到的类型推导、引用折叠(reference collapsing),详见C++11中的通用引用。这里先举例说明其特点:
例1::
1 | int i = 42; |
例2::
1 | char&& var1 = 'A'; |
注意,虽然var1
的类型是右值引用(char&&
),但它本身是一个左值,这一点在C++11中的右值引用中被反复提到。所以,这里var2
引用的是左值,auto
被推导为char&
,var2
的类型是char& &&
,最终折叠为char&
。乍一看,处处是&&
,但最终var1
和var2
却都是左值引用,它们引用相同的地址:中间两行打印的地址是一样的;最后两行都输出’B’(这个地址被改写成’B’)。
使用场景 (2)
基础用法 (2.1)
1 | std::vector<int> vect; |
可以更简便的写作:
1 | std::vector<int> vect; |
变量it和vect.begin()的类型是完全一样的,因此,编译器可以推断出它的类型,不必麻烦程序员每次都重写一遍。还有其他类似的使用方式,其目的是让编译器来推断类型,程序员省去一些打字的工作(毕竟,写个auto是很快捷的)。
然而,本人不喜欢这样的用法,理由是(本人的拙见,欢迎持不同意见的同学来讨论):
- 打字在编程中占的工作量有限,在这方面节省一点时间,能提高多少效率呢?
- 程序员在写代码的时候,实际上是很清楚每个变量的类型的,这个时候为了节省一点打字工作而简单的写个auto,维护(阅读)代码的人可能需要自己去推导变量的类型。而这个推导成本,应该远远大于打字节省的工作量吧!
- 即使对于上面的例子(for循环遍历vector),明确的写出vector< int >::iterator(或者vector< int >::const_iterator),也可以让程序员熟悉vector的使用方式,甚至能够使程序员联想起vector以及iterator的实现(类似的,map及其iterator的实现……)。
所以,本人认为,仅仅为了编码便利,我们不应该使用auto;它应该出现在有正当需求的地方,见下文。
无法确定类型的地方 (2.2)
比如,我们写这样一个模板函数:
1 | template<typename T, typename S> |
C++11以前,表达式lhs*rhs的类型无法确定(若lhs都是int,则为int;若一个int一个double则为double;另外,T和S还可能是程序员自定义的类——重载了乘法运算符——那么结果类型就有无限多了),我们该如何写prod的类型呢?
C++11有两种途径来定义prod的类型,其一就是如上所示的auto。另一个是decltype,见C++11中的decltype关键字。
Function return type deduction in C++14 (2.3)
C++11中提供了一个特性:lambda函数的返回类型可以由return表达式的类型推定。C++14把这一特性扩展到所有函数:也就是,函数的返回类型定义为auto(注意,后面没有 ->decltype(),若有,则是C++11的Trailing return type语法,见C++11中的decltype关键字)。例如:
1 | auto add(int a, int b) |
和前文(基础用法)类似,编译器可以根据return语句推断出函数的类型。同样,我不喜欢这样的写法,理由也类似。除此之外,它还有一些限制:
- 限制1:若有多个返回语句,则各个返回表达式的类型必须一样;
- 限制2:可以前置声明,但是定义之前是不能使用的(那前置声明有什么用呢?);
- 限制3:返回auto的函数递归时,递归调用前,至少有一个return 语句。例如:
1 | auto Correct(int i) |
Generic lambdas in C++14 (2.4)
在C++11中,lambda函数的参数只能是具体类型,而在C++14中,lambda函数的参数可以是auto类型。例如:
1 | auto lambda = [](auto x, auto y) {return x + y;}; |
我们不关注auto lambda
中的auto
,它的推导结果是一个Functor类(对吧?需要研究lambda的实现)。把焦点放在auto x
和auto y
上,它们的推导和模板类型参数的推导类似。上面的代码等价于:
1 | struct unnamed_lambda |
Trailing return type (2.5)
这是decltype的使用方式,auto只是配合一下,类型的推导完全按decltype的规则进行。见C++11中的decltype关键字。
Alternate type deduction on declaration in C++14 (2.6)
这是auto和decltype结合的使用方式。见C++11中的decltype关键字。
小结 (3)
本文介绍了C++11引入的auto关键字,包括类型推导规则、使用场景。和它类似、又相关的decltype,将在C++11中的decltype关键字中介绍。