4.2.3 auto的使用细则

虽然我们在4.2.1节及4.2.2节中看到了很多关于auto的使用,不过auto使用上还有很多语法相关的细节。如果读者在使用auto的时候遇到一些不理解的状况,不妨回头来查阅一下这些规则。

首先我们可以看看auto类型指示符与指针和引用之间的关系。在C++11中,auto可以与指针和引用结合起来使用,使用的效果基本上会符合C/C++程序员的想象。我们可以看看代码清单4-11所示的例子。

代码清单4-11


int x;

int*y=&x;

double foo();

int&bar();

autoa=&x;//int

auto&b=x;//int&

auto c=y;//int*

autod=y;//int

auto*e=&foo();//编译失败,指针不能指向一个临时变量

auto&f=foo();//编译失败,nonconst的左值引用不能和一个临时变量绑定

auto g=bar();//int

auto&h=bar();//int&

//编译选项:g++ -std=c++11 4-2-9.cpp


本例中,变量a、c、d的类型都是指针类型,且都指向变量x。实际上对于a、c、d三个变量而言,声明其为auto*或auto并没有区别。而如果要使得auto声明的变量是另一个变量的引用,则必须使用auto&,如同本例中的变量b和h一样。

其次,auto与volatile和const之间也存在着一些相互的联系。volatile和const代表了变量的两种不同的属性:易失的和常量的。在C++标准中,它们常常被一起叫作cv限制符(cv-qualifier)。鉴于cv限制符的特殊性,C++11标准规定auto可以与cv限制符一起使用,不过声明为auto的变量并不能从其初始化表达式中“带走”cv限制符。我们可以看看代码清单4-12所示的例子。

代码清单4-12


double foo();

float*bar();

const auto a=foo();//a:const double

const auto&b=foo();//b:const double&

volatile autoc=bar();//c:volatile float

auto d=a;//d:double

auto&e=a;//e:const double&

auto f=c;//f:float*

volatile auto&g=c;//g:volatile float*&

//编译选项:g++ -std=c++11 4-2-10.cpp


在代码清单4-12中,我们可以通过非cv限制的类型初始化一个cv限制的类型,如变量a、b、c所示。不过通过auto声明的变量d、f却无法带走a和f的常量性或者易失性。这里的例外还是引用,可以看出,声明为引用的变量e、g都保持了其引用的对象相同的属性(事实上,指针也是一样的)。

此外,跟其他的变量指示符一样,同一个赋值语句中,auto可以用来声明多个变量的类型,不过这些变量的类型必须相同。如果这些变量的类型不相同,编译器则会报错。事实上,用auto来声明多个变量类型时,只有第一个变量用于auto的类型推导,然后推导出来的数据类型被作用于其他的变量。所以不允许这些变量的类型不相同,如代码清单4-13所示。

代码清单4-13


auto x=1,y=2;//x和y的类型均为int

//m是一个指向const int类型变量的指针,n是一个int类型的变量

const auto*m=&x,n=1;

auto i=1,j=3.14f;//编译失败

auto o=1,&p=o,*q=&p;//从左向右推导

//编译选项:g++ -std=c++11 4-2-11.cpp-c


在代码清单4-13中,我们使用auto声明了两个类型相同变量x和y,并用逗号进行分隔,这可以通过编译。而在声明变量i和j的时候,按照我们所说的第一变量用于推导类型的规则,那么由于x所推导出的类型是int,那么对于变量j而言,其声明就变成了int j=3.14f,这无疑会导致精度的损失。而对于变量m和n,就变得非常有趣,这里似乎是auto被替换成了int,所以m是一个int*指针类型,而n只是一个int类型。同样的情况也发生在变量o、p、q上,这里o是一个类型为int的变量,p是o的引用,而q是p的指针。auto的类型推导按照从左往右,且类似于字面替换的方式进行。事实上,标准里称auto是一个将要推导出的类型的“占位符”(placeholder)。这样的规则无疑是直观而让人略感意外的。当然,为了不必要的繁琐记忆,程序员可以选择每一个auto变量的声明写成一行(有些观点也认为这是好的编程规范)。

此外,只要能够进行推导的地方,C++11都为auto指定了详细的规则,保证编译器能够正确地推导出变量的类型。包括C++11新引入的初始化列表,以及new,都可以使用auto关键字,如代码清单4-14所示。

代码清单4-14


include <initializer_list>

auto x=1;

auto x1(1);

auto y{1};//使用初始化列表的auto

auto z=new auto(1);//可以用于new

//编译选项:g++ -std=c++11-c 4-2-12.cpp


代码清单4-14中,auto变量y的初始化使用了初始化列表,编译器可以保证y的类型推导为int。而z指针所指向的堆变量在分配时依然选择让编译器对类型进行推导,同样的,编译器也能够保证这种方式下类型推导的正确性。

不过auto也不是万能的,受制于语法的二义性,或者是实现的困难性,auto往往也会有使用上的限制。这些例外的情况都写在了代码清单4-15所示的例子当中。

代码清单4-15


include <vector>

using namespace std;

void fun(auto x=1){}//1:auto函数参数,无法通过编译

struct str{

auto var=10;//2:auto非静态成员变量,无法通过编译

};

int main(){

char x[3];

auto y=x;

auto z[3]=x;//3:auto数组,无法通过编译

//4:auto模板参数(实例化时),无法通过编译

vector<auto> v={1};

}

//编译选项:g++ -std=c++11 4-2-13.cpp


我们分别来看看代码清单4-15中的4种不能推导的情况。

1)对于函数fun来说,auto不能是其形参类型。可能读者感觉对于fun来说,由于其有默认参数,所以应该推导fun形参x的类型为int型。但事实却无法符合大家的想象。因为auto是不能做形参的类型的。如果程序员需要泛型的参数,还是需要求助于模板。

2)对于结构体来说,非静态成员变量的类型不能是auto的。同样的,由于var定义了初始值,读者可能认为auto可以推导str成员var的类型为int的。但编译器阻止auto对结构体中的非静态成员进行推导,即使成员拥有初始值。

3)声明auto数组。我们可以看到,main中的x是一个数组,y的类型是可以推导的,而声明auto z[3]这样的数组同样会被编译器禁止。

4)在实例化模板的时候使用auto作为模板参数,如main中我们声明的vector<auto> v。虽然读者可能认为这里一眼而知是int类型,但编译器却阻止了编译。

以上4种情况的特点基本相似,人为地观察很容易能够推导出auto所在位置应有的类型,但现有的C++11的标准还没有支持这样的使用方式。如果程序员遇到auto不够聪明的情况,不妨回头看看是否违背了以上一些规则。

此外,程序员还应该注意,由于为了避免和C++98中auto的含义发生混淆,C++11只保留auto作为类型指示符的用法,以下的语句在C++98和C语言中都是合法的,但在C++11中,编译器则会报错。


auto int i=1;


总的来说,auto在C++11中是相当关键的特性之一。我们之后还会在很多地方看到auto,比如4.4节中的追踪返回类型的函数声明,以及7.3节中lambda与auto的配合使用等。(事实上,第3章中我们也使用过)。不过,如我们提到的,auto只是C++11中类型推导体现的一部分。其余的,则会在decltype中得到体现。