4.2.2 auto的优势

直观地,auto推导的一个最大优势就是在拥有初始化表达式的复杂类型变量声明时简化代码。由于C++的发展,声明变量类型也变得越来越复杂,很多时候,名字空间、模板成为了类型的一部分,导致程序员在使用库的时候如履薄冰。我们可以看看代码清单4-5所示的例子。

代码清单4-5


include <string>

include <vector>

void loopover(std::vector<std::string>&vs){

std::vector<std::string>::iterator i=vs.begin();//想要使用iterator,往往需要书写大量代码

for(;i<vs.end();i++){

//一些代码

}

}

//编译选项:g++ -c 4-2-3.cpp


代码清单4-5中,我们在不使用using namespace std的情况下(事实上,很多专家的建议就是如此)想对一个vector数组进行循环。可以看到,当我们想定义一个迭代器i的时候,我们必须写出std::vector<std::string>::iterator这样长的类型声明。即使是一位擅长克服各种困难的C++老手,也不会对如此冗长的代码视而不见。而使用auto的话,代码会的可读性可以成倍增长,如代码清单4-6所示。

代码清单4-6


include <string>

include <vector>

void loopover(std::vector<std::string>&vs){

for(auto i=vs.begin();i<vs.end();i++){

//一些代码

}

}

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


如我们所见,使用了auto,程序员甚至可以将i的声明放入for循环中,i的类型将由表达式vs.begin()推导出。事实上,形如代码清单4-5的复杂声明在使用STL的代码中处处可见,在C++11中,由于auto的存在,使用STL将会变得更加容易,写出的代码也会更加清晰可读。

auto的第二个优势则在于可以免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误。事实上,在C/C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回的是整型,这是一条隐式规则)。这些规则并非很容易记忆,尤其是在用户自定义了很多操作符之后。而这个时候,auto就有用武之地了。我们可以看看代码清单4-7所示的例子。

代码清单4-7


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(piradius);

}

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


代码清单4-7中,定义了float型的变量radius(半径)以及一个自定义类型PI变量pi(π值),在计算圆周长的时候,使用了auto类型来定义变量circumference。这里,PI在与float类型数据相乘时,其返回值为double。而PI的定义可能是在其他的地方(头文件里),main函数的程序员可能不知道PI的作者为了避免数据上溢或者精度降低而返回了double类型的浮点数。因此main函数程序员如果使用float类型声明circumference,就可能享受不了PI作者细心设计带来的好处。反之,将circumference声明为auto,则毫无问题,因为编译器已经自动地做了最好的选择。

值得指出的是,auto并不能解决所有的精度问题,我们可以看看代码清单4-8所示的例子。

代码清单4-8


include <iostream>

using namespace std;

int main(){

unsigned int a=4294967295;//最大的unsigned int值

unsigned int b=1;

auto c=a+b;//c的类型依然是unsigned int

cout<<"a="<<a<<endl;//a=4294967295

cout<<"b="<<b<<endl;//b=1

cout<<"a+b="<<c<<endl;//a+b=0

return 0;

}

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


在代码清单4-8中,程序员可能指望通过声明变量c为auto就能解决a+b溢出的问题。而实际上由于a+b返回的依然是unsigned int的值,故而c的类型依然被推导为unsigned int,auto并不能帮上忙。这跟一些动态类型语言中数据会自动进行扩展的特性还是不一样的。

auto的第三个优点就是其“自适应”性能够在一定程度上支持泛型的编程。

我们再回到代码清单4-7的例子,这里假设PI的作者改动了PI的定义,比如将operator*返回值变为long double,此时,main函数并不需要修改,因为auto会“自适应”新的类型。

同理,对于不同的平台上的代码维护,auto也会带来一些“泛型”的好处。这里我们以strlen函数为例,在32位的编译环境下,strlen返回的为一个4字节的整型,而在64位的编译环境下,strlen会返回一个8字节的整型。虽然系统库<cstring>为其提供了size_t类型来支持多平台间的代码共享支持,但是使用auto关键字我们同样可以达到代码跨平台的效果。


auto var=strlen("hello world!").


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

当auto应用于模板的定义中,其“自适应”性会得到更加充分的体现。我们可以看看代码清单4-9所示的例子。

代码清单4-9


template<typename T1,typename T2>

double Sum(T1&t1,T2&t2){

auto s=t1+t2;//s的类型会在模板实例化时被推导出来

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

}

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


在代码清单4-9中,Sum模板函数接受两个参数。由于类型T1、T2要在模板实例化时才能确定,所以在Sum中将变量s的类型声明为auto的。在函数main中我们将模板实例化时,Sum<int,long>中的s变量会被推导为long类型,而Sum<float,float>中的s变量则会被推导为float。可以看到,auto与模板一起使用时,其“自适应”特性能够加强C++中“泛型”的能力。不过在这个例子中,由于总是返回double类型的数据,所以Sum模板函数的适用范围还是受到了一定的限制,在4.4节中,我们可以看到怎么使用追踪返回类型的函数声明来完全释放Sum泛型的能量。

另外,应用auto还会在一些情况下取得意想不到的好效果。我们可以看看代码清单4-10所示的例子[1]

代码清单4-10


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(123*4,5+6+7+8);

int m2=Max2(123*4,5+6+7+8);

}

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


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

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

[1]本例素材来源于GNU C的手册http://gcc.gnu.org/onlinedocs/gcc/Typeof.html。