0%

C++11中的auto关键字

本文介绍了auto关键字在C++11(及C++14)中的类型推导规则和使用场景。有些地方它不可或缺,但也要避免滥用。

类型推导 (1)

引用和修饰符剥除 (1.1)

在看auto类型推导之前,先看看模板类型参数的推导:

1
2
3
4
5
6
template<class T>
void func(T arg);

int i = 10;
const int& cri = i;
func(cri);

func(cri)中,类型参数T最终是什么呢?答案是int,也就是cri声明中的const&都被忽略了。要想保留有两个办法:

办法1:

1
func<const int&>(crx);

办法2:

1
2
template<class T>
void func(const T& arg);

这里,const&(以及&&)被忽略的现象叫做stripping,这里翻译为剥除吧。

另外,注意下面这种情况,const不会被剥除,因为一旦剥除,const值就变的可修改了:

1
2
3
int j = 20;
const int* p = &j;
func(p); //T的类型是"const int*","const"不能被剥除;

auto推导和上面模板类型参数的推导很相似。对于:

1
auto a = {expression};
  • 如果{expression}有引用(无论是左值引用还是右值引用),则引用修饰符(&或者&&)被剥除;
  • 如果{expression}有top-level的const/volatile修饰符,则const/volatile被剥除(stripping);

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 0;
const int & cri = i;
auto a1 = cri; //a1的类型是"int",因为左值引用"&"和"const"被剥除了;
a1 = 1; //OK

const int && crvref = 10;
auto a2 = crvref; //a2的类型是"int",因为右值引用"&&"和"const"被剥除了;
a2 = 2; //OK

int x = 42;
const int * p1 = &x;
auto p2 = p1; //p2的类型是"const int*","const"没有被剥除,因为它不是top-level的;
*p2 = 43; //error: assignment of read-only location '* p2'

可见,和模板类型参数的推导类似,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const auto& r1 = 1;

int i = 2;
const auto& r2 = i;

int& ri = i;
const auto& r3 = ri;

const int ci = 3;
const auto& r4 = ci;

r1 = 100; //error: assignment of read-only reference 'r1'
r2 = 100; //error: assignment of read-only reference 'r2'
r3 = 100; //error: assignment of read-only reference 'r3'
r4 = 100; //error: assignment of read-only reference 'r4'

由于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
2
3
4
5
6
7
8
9
10
11
12
const int c = 2;
auto& rc1 = c; //rc1的类型是"const int&";因为const不能被剥除,否则c就能够被修改;

int i = 1;
const int& ri = i;
auto& rc2 = ri; //rc2的类型是"const int&";ri的"&"可以剥除但const不能被剥除,否则ri就能够被修改;

rc1 = 100; //error: assignment of read-only reference rc1
rc2 = 100; //error: assignment of read-only reference rc2

//必须引用左值;
auto& rc3 = 4; //error: invalid initialization of non-const reference of type int& from an rvalue of type int

const auto&& (1.4)

1
const auto&& a = {expression};

这种情况也比较简单,结果a的类型就是一个const的右值引用。

1
2
3
4
5
6
7
8
9
int lv = 3;
//不能引用左值;
const auto&& r1 = lv; //error: cannot bind int lvalue to const int&&

//只能引用右值
const auto&& r2 = 4;

//由于const,所以不能修改;
r2 = 5; //error: assignment of read-only reference r2

注意,虽然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的类型推导结果是Ta的类型是T&&;const可剥除(const的右值有意义吗?);

虽然”&&”很容易让人想起右值引用,但它不是!你给它左值,它表现出左值引用的行为;给它右值,它表现出右值引用的行为,这种特殊的引用叫作通用引用。关于通用引用和上面提到的类型推导、引用折叠(reference collapsing),详见C++11中的通用引用。这里先举例说明其特点:

例1::

1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 42;
auto&& r1 = i; //引用左值,故auto的推导结果是"int&",r1的类型是"int& &&",折叠成"int&"
r1 = 100;

const int c = 43;
auto&& r2 = c; //同上,但const不能剥除;
r2 = 101; //error: assignment of read-only reference r2

auto&& r3 = 44; //引用右值,故auto的推导结果是"int", r3的类型是"int&&";
r3 = 102; //OK

auto&& r4 = (const int)44; //同上,但const能剥除;
r4 = 103; //OK

例2::

1
2
3
4
5
6
7
8
9
10
char&& var1 = 'A';
auto&& var2 = var1;

std::cout << (void*)&var1 << std::endl;
std::cout << (void*)&var2 << std::endl;

var2 = 'B';

std::cout << var1 << std::endl;
std::cout << var2 << std::endl;

注意,虽然var1的类型是右值引用(char&&),但它本身是一个左值,这一点在C++11中的右值引用中被反复提到。所以,这里var2引用的是左值,auto被推导为char&var2的类型是char& &&,最终折叠为char&。乍一看,处处是&&,但最终var1var2却都是左值引用,它们引用相同的地址:中间两行打印的地址是一样的;最后两行都输出’B’(这个地址被改写成’B’)。

使用场景 (2)

基础用法 (2.1)

1
2
3
4
5
6
std::vector<int> vect;
// ...
for(std::vector<int>::iterator it = vect.begin(); it != vect.end(); ++it)
{
std::cin >> *it;
}

可以更简便的写作:

1
2
3
4
5
6
std::vector<int> vect;
// ...
for(auto it = vect.begin(); it != vect.end(); ++it)
{
std::cin >> *it;
}

变量it和vect.begin()的类型是完全一样的,因此,编译器可以推断出它的类型,不必麻烦程序员每次都重写一遍。还有其他类似的使用方式,其目的是让编译器来推断类型,程序员省去一些打字的工作(毕竟,写个auto是很快捷的)。

然而,本人不喜欢这样的用法,理由是(本人的拙见,欢迎持不同意见的同学来讨论):

  • 打字在编程中占的工作量有限,在这方面节省一点时间,能提高多少效率呢?
  • 程序员在写代码的时候,实际上是很清楚每个变量的类型的,这个时候为了节省一点打字工作而简单的写个auto,维护(阅读)代码的人可能需要自己去推导变量的类型。而这个推导成本,应该远远大于打字节省的工作量吧!
  • 即使对于上面的例子(for循环遍历vector),明确的写出vector< int >::iterator(或者vector< int >::const_iterator),也可以让程序员熟悉vector的使用方式,甚至能够使程序员联想起vector以及iterator的实现(类似的,map及其iterator的实现……)。

所以,本人认为,仅仅为了编码便利,我们不应该使用auto;它应该出现在有正当需求的地方,见下文。

无法确定类型的地方 (2.2)

比如,我们写这样一个模板函数:

1
2
3
4
5
6
template<typename T, typename S>
void foo(T lhs, S rhs)
{
auto prod = lhs * rhs;
//...
}

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
2
3
4
auto add(int a, int b)
{
return a + b;
}

和前文(基础用法)类似,编译器可以根据return语句推断出函数的类型。同样,我不喜欢这样的写法,理由也类似。除此之外,它还有一些限制:

  • 限制1:若有多个返回语句,则各个返回表达式的类型必须一样;
  • 限制2:可以前置声明,但是定义之前是不能使用的(那前置声明有什么用呢?);
  • 限制3:返回auto的函数递归时,递归调用前,至少有一个return 语句。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto Correct(int i)
{
if (i == 1)
return i; // return type deduced as int
else
return Correct(i-1)+i; // ok to call it now
}

auto Wrong(int i)
{
if (i != 1)
return Wrong(i-1)+i; // Too soon to call this. No prior return statement.
else
return i; // return type deduced as int
}

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 xauto y上,它们的推导和模板类型参数的推导类似。上面的代码等价于:

1
2
3
4
5
6
struct unnamed_lambda
{
template<typename T, typename U>
auto operator()(T x, U y) const {return x + y;}
};
auto lambda = 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关键字中介绍。

写的不错,有赏!