一、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
2
3
4
5
6
7
8
9
10
11
12
int main()
{
double foo();
auto x = 1; //x的类型为int
auto y = foo(); //y的类型为double
struct m
{
int i;
}str;
auto str1 = str; //str1的类型是sturct m
//auto z; //无法推导,不能通过编译
}

auto 声明的变量必须被初始化,以使编译能够从其初始化表达式中推导出其类型。这里可以理解为auto并非一种“类型”,而是一个类型声明时的“占位符”,编译器在编译时会将auto替代为变量实际的类型。

auto的优势

①、最大优势就是在拥有初始化表达式的复杂类型变量声明时简化代码

由于C++的发展,声明变量类型也变得越来越复杂,很多时候,名字空间、模板成为了类型的一部分,导致程序员在使用库的时候如履薄冰。

1
2
3
4
5
6
7
8
9
10
#include <string>
#incldue <vector>
void loopover(std::vector<std:string> & vs)
{
std::vector<std::string>::iterator i = vs.begin(); //可看出在在不使用命名空间时,使用iterator 需要书写大量代码
for(; i < vs.end(); i++)
{
...
}
}

用auto的话,代码会的可读性可以成倍增长,如下所示:

1
2
3
4
5
6
7
8
9
10
#include <string>
#incldue <vector>
void loopover(std::vector<std:string> & vs)
{
std::vector<std::string>::iterator i = vs.begin();
for(auto i = vs.begin(); i < vs.end(); i++)
{
...
}
}

②、第二个优势则在于可以免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误

在C/C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回的是整型,这是一条隐式规则)。这些规则并非很容易记忆,尤其是在用户自定义了很多操作符之后。而这个时候,auto就有用武之地了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PI
{
public:
double operator* (float v)
{
return (double)val * v;
}
const float val = 3.1415927f;
};

int main()
{
float radius = 1.7e10;
PI pi;
auto circumference = 2 * (pi * radius);
cout << "circumference = " << circumference << endl;
return 0;
}
1
2
输出:
circumference = 1.06814e+11

这里定义了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字节的整型。虽然系统库为其提供了size_t类型来支持多平台间的代码共享支持,但是使用auto关键字我们同样可以达到代码跨平台的效果。

1
auto v = strlen("hello world!")

由于size_t的适用范围往往局限于中定义的函数,auto的适用范围明显更为广泛。

当auto应用于模板的定义中,其“自适应”性会得到更加充分的体现。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

template<typename T1, typename T2>
double Sum(T1 & t1, T2 & t2)
{
auto s = t1 + t2;
return s;
}

int main()
{
int a = 3;
long b = 5;
float c = 1.0f, d = 2.3f;
auto e = Sum<int,long>(a,b);//s的类型被推导为long
auto f = Sum<float,float>(c,d);//s的类型被推导为float
cout << e << endl;
cout << f << endl;
return 0;
}
1
2
3
输出:
8
3.3

在上面程序中,由于类型T1、T2要在模板实例化时才能确定,所以在Sum中将变量s的类型声明为auto的。在函数main中我们将模板实例化时,Sum<int,long>中的s变量会被推导为long类型,而Sum<float, float>中的s变量则会被推导为float。可以看到,auto与模板一起使用时,其“自适应”特性能够加强C++中“泛型”的能力。不过在这个例子中,由于总是返回double类型的数据,所以Sum模板函数的适用范围还是受到了一定的限制。

④、在宏定义中,避免出现性能问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

#define MAX1(a, b) ((a) > (b)) ? (a) : (b)
#define MAX2(a, b) ({\
auto _a = (a);\
auto _b = (b);\
(_a > _b) ? _a : _b;})

int main()
{
int m1 = MAX1(1*2*3*4, 5+6+7+8);
int m2 = MAX2(1*2*3*4, 5+6+7+8);
cout << m1 << endl;
cout << m2 << endl;
return 0;
}

定义了两种类型的宏Max1和Max2。两者作用相同,都是求a和b中较大者并返回。前者采用传统的三元运算符表达式,这可能会带来一定的性能问题。因为a或者b在三元运算符中都出现了两次,那么无论是取a还是取b,其中之一都会被运算两次。而在Max2中,我们将a和b都先算出来,再使用三元运算符进行比较,就不会存在这样的问题了。

在传统的C++98标准中,由于a和b的类型无法获得,所以我们无法定义Max2这样高性能的宏。而新的标准中的auto则提供了这种可行性。

auto使用时注意事项

auto类型指示符与指针和引用之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
int x = 1;
int * y = &x;
double foo();
int & bar();
auto * a = &x; // int*
auto & b = x;// int&
auto c = y;// int*
auto * d = y; // int*
//auto * e = &foo();//编译失败,指针不能指向一个临时变量
//auto & f = foo();//编译失败,nonconst的左值引用不能和一个临时变量绑定
auto g = bar();// int
auto & h = bar();// int&

变量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
2
3
4
5
6
7
8
9
double foo();
float * bar();
const auto a = foo(); //a:const double
const auto & b = foo(); //b:const double&
volatile auto * c = bar(); //c:volatile float*
auto d = a; //d:double
auto & e = e; //e:const double &
auto f = c; //f:float *
volatile auto & g = c; //g:volatile float * &

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

auto可以用来声明多个变量的类型,不过这些变量的类型必须相同

如果这些变量的类型不相同,编译器则会报错。事实上,用auto来声明多个变量类型时,只有第一个变量用于auto的类型推导,然后推导出来的数据类型被作用于其他的变量。

1
2
3
4
5
auto x = 1, y = 2;
//m是一个指向const int类型变量的指针,n是一个int类型的变量
const auto* m = &x, n = 1;
//auto i = 1, j = 3.14f; //编译失败
auto o = 1,&p = o,*q = &p; //从左向右推导

使用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
2
3
4
5
#include <initializer_list>
auto x = 1;
auto x1(1);
auto y {1}; // 使用初始化列表的auto
auto z = new auto(1); // 可以用于new

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

不过auto也不是万能的,受制于语法的二义性,或者是实现的困难性,auto往往也会有使用上的限制

1
2
3
4
5
6
7
8
9
10
11
12
#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};
}

①、对于函数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类型,但编译器却阻止了编译。