一、C++11新特性:auto类型推导
一、auto类型推导
声明:该笔记是在学习《深入理解C++11》、《C++11/14高级编程 Boost程序库探秘》时做的总结,方便以后巩固复习!
静态类型、动态类型和类型推导
静态类型:C/C++常被成为静态类型的编程语言,变量必须被定义;
动态类型:python、Perl、JavaScript语言常被称为动态类型的编程语言,变量不需要声明就可以被使用。
静态类型和动态类型的区别:是在对变量进行类型检测的时间点;静态类型的类型检测主要发生在编译阶段;动态类型的类型检测主要发生在运行阶段。
动态类型语言变量“拿来就用”的特性依赖的是类型推导技术;事实上类型推导也可以用于静态类型的语言中;C++11中类型推导的实现方式就有两种:①、auto,②、decltype;先学习auto关键字!
auto关键字在早期的C/C++标准中的含义:
按照C/C++早期标准,声明时使用auto修饰的变量,是具有自动存储的局部变量;然而几乎无人使用这种含义,因为一般函数内没有被声明为static的变量总是具有自动存储的局部变量。
auto关键字在C++11中的含义:
auto不再是一个存储类型指示符(如static、extern为纯粹类型指示符),而是一个新的类型指示符(int、float等是类型指示符)来指示编译器,auto声明变量的类型必须由编译器在编译时期推导而得。
1 | int main() |
auto 声明的变量必须被初始化,以使编译能够从其初始化表达式中推导出其类型。这里可以理解为auto并非一种“类型”,而是一个类型声明时的“占位符”,编译器在编译时会将auto替代为变量实际的类型。
auto的优势
①、最大优势就是在拥有初始化表达式的复杂类型变量声明时简化代码
由于C++的发展,声明变量类型也变得越来越复杂,很多时候,名字空间、模板成为了类型的一部分,导致程序员在使用库的时候如履薄冰。
1 |
|
用auto的话,代码会的可读性可以成倍增长,如下所示:
1 |
|
②、第二个优势则在于可以免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误
在C/C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回的是整型,这是一条隐式规则)。这些规则并非很容易记忆,尤其是在用户自定义了很多操作符之后。而这个时候,auto就有用武之地了。
1 | class PI |
1 | 输出: |
这里定义了float型的变量radius(半径)以及一个自定义类型PI变量pi(π值),在计算圆周长的时候,使用了auto类型来定义变量circumference。这里,PI在与float类型数据相乘时,其返回值为double。而PI的定义可能是在其他的地方(头文件里),main函数的程序员可能不知道PI的作者为了避免数据上溢或者精度降低而返回了double类型的浮点数。因此main函数程序员如果使用float类型声明circumference,就可能享受不了PI作者细心设计带来的好处。反之,将circumference声明为auto,则毫无问题,因为编译器已经自动地做了最好的选择。
③、第三个优点就是其“自适应”性能够在一定程度上支持泛型的编程
再回到上面代码例子,这里假设改动了PI的定义,如将operator*返回值变为long double,此时,main函数并不需要修改,因为auto会“自适应”新的类型。
同时,对于不同的平台上的代码维护,auto也会带来一些“泛型”的好处。这里我们以strlen函数为例,在32位的编译环境下,strlen返回的为一个4字节的整型,而在64位的编译环境下,strlen会返回一个8字节的整型。虽然系统库
1 | auto v = strlen("hello world!") |
由于size_t的适用范围往往局限于
当auto应用于模板的定义中,其“自适应”性会得到更加充分的体现。如:
1 |
|
1 | 输出: |
在上面程序中,由于类型T1、T2要在模板实例化时才能确定,所以在Sum中将变量s的类型声明为auto的。在函数main中我们将模板实例化时,Sum<int,long>中的s变量会被推导为long类型,而Sum<float, float>中的s变量则会被推导为float。可以看到,auto与模板一起使用时,其“自适应”特性能够加强C++中“泛型”的能力。不过在这个例子中,由于总是返回double类型的数据,所以Sum模板函数的适用范围还是受到了一定的限制。
④、在宏定义中,避免出现性能问题
1 |
|
定义了两种类型的宏Max1和Max2。两者作用相同,都是求a和b中较大者并返回。前者采用传统的三元运算符表达式,这可能会带来一定的性能问题。因为a或者b在三元运算符中都出现了两次,那么无论是取a还是取b,其中之一都会被运算两次。而在Max2中,我们将a和b都先算出来,再使用三元运算符进行比较,就不会存在这样的问题了。
在传统的C++98标准中,由于a和b的类型无法获得,所以我们无法定义Max2这样高性能的宏。而新的标准中的auto则提供了这种可行性。
auto使用时注意事项
auto类型指示符与指针和引用之间的关系
1 | int x = 1; |
变量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限制符。
1 | double foo(); |
可以看出通过非cv限制的类型初始化一个cv限制的类型,如变量a、b、c所示。不过通过auto声明的变量d、f却无法带走a和f的常量性或者易失性。这里的例外还是引用,可以看出,声明为引用的变量e、g都保持了其引用的对象相同的属性(事实上,指针也是一样的)。
auto可以用来声明多个变量的类型,不过这些变量的类型必须相同
如果这些变量的类型不相同,编译器则会报错。事实上,用auto来声明多个变量类型时,只有第一个变量用于auto的类型推导,然后推导出来的数据类型被作用于其他的变量。
1 | auto x = 1, y = 2; |
使用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关键字
1 |
|
auto变量y的初始化使用了初始化列表,编译器可以保证y的类型推导为int。而z指针所指向的堆变量在分配时依然选择让编译器对类型进行推导,同样的,编译器也能够保证这种方式下类型推导的正确性。
不过auto也不是万能的,受制于语法的二义性,或者是实现的困难性,auto往往也会有使用上的限制
1 |
|
①、对于函数fun来说,auto不能是其形参类型。可能读者感觉对于fun来说,由于其有默认参数,所以应该推导fun形参x的类型为int型。但事实却无法符合大家的想象。因为auto是不能做形参的类型的。如果程序员需要泛型的参数,还是需要求助于模板。
②、对于结构体来说,非静态成员变量的类型不能是auto的。同样的,由于var定义了初始值,读者可能认为auto可以推导str成员var的类型为int的。但编译器阻止auto对结构体中的非静态成员进行推导,即使成员拥有初始值。
③、声明auto数组。我们可以看到,main中的x是一个数组,y的类型是可以推导的,而声明auto z[3]这样的数组同样会被编译器禁止。
④、在实例化模板的时候使用auto作为模板参数,如main中我们声明的vector
v。虽然读者可能认为这里一眼而知是int类型,但编译器却阻止了编译。