一、静态类型,动态类型和类型推导
在编程语言分类中,C/C++C常常被认为是静态类型的语言。而有的编程语言则号称是“动态类型”的,比如python。通常情况下,“静”和“动”的区别是非常直观的。我们看看下面这段简单的python代码:
name=‘world\n’ print 'hello, ' %name
这段代码中python中的一个hellowworld的实现。这就是编程语言中的“动态类型”,在运行时来进行类型检查,而C++中类型检查是在编译阶段。动态类型语言能做到在运行时决定类型,主要归功于一技术,这技术是类型推导。
事实上,类型推导也可以用于静态类型语言中。比如上面的python代码中,如果按照C/C++程序员的思考方式,world\n表达式应该可以返回一个临时的字符串,所以即使name没有进行声明,我们也能轻松低推导出name的类型应该是一个字符串类型。在C++11中,这个想法得到了实现。C++11中类型推导的实现之一就是重定义auto关键字,另一个实现是decltype。
我们可以使用C++11方式来书写刚才的python的代码
#include <iostream> using namespace std; int main() { auto name=‘world\n’ cout<<"hello "<<name<<endl; }
这里使用auto关键字来要求编译器对变量name的类型进行了自动推导。这里编译器根据它的初始化表达式的类型,推导出name的类型为char*。事实上,atuo关键字在早期的C/C++标准中有着完全不同的含义。声明时使用auto修饰的变量,按照早期C/C++标准的解释,是具有自动存储期的局部变量。不过系那是情况是该关键字几乎无人使用,因为一般函数内没有声明为static的变量总是具有自动存储期的局部变量。auto声明变量的类型必须又编译器在编译时期推导而得。
通过以下例子来了解以下auto类型推导的基本用法
#include <iostream> using namespace std; int main() { double foo(); auto x=1; auto y=foo(); struct m { int i; }str; auto str1=str; auto z; z=x; }
以上变量x被初始化为1,因为字面变量1的类型是const int,所以编译器推导出x的类型应该为int(这里const类型限制符被去掉了,后面会解释)。同理在变量y的定义中,auto类型的y被推导为double类型;而在auto str1的定义中,其类型被推导为struct m。这里的z,使用auto关键字来声明,但是不立即对其进行定义,此时编译器则会报错。这跟通过其他关键字(除去引用类型的关键字)先声明后定义的变量的使用规则是不同的。auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。这个意义上,auto并非一种类型声明,而是一个类型声明时的“占位符”,编译器在便已是亲会将suto替代为变量实际的类型。
二、auto的优势
1.直观地,auto推导的一个最大的优势在于拥有初始化表达式的复杂类型变量声明时简化代码。由于C++的发展,变量类型变得越来越复杂。但是很多时候,名字空间、模板成为类型的一部分,导致了程序员在使用库的时候如履薄冰。
#include <string> #include <vector> void loopover(std::vector<std::string>&vs) { std::vector<std::string>::iterator i=vs.begin(); for(;i<vs.end();i++) { } }
使用std::vector<std::string>::iterator 来定义i是C++常用的良好的习惯,但是这样长的声明带来了代码可读性的困难,因此引入auto,使代码可读性增加。并且使用STL将会变得更加容易
<pre name="code" class="cpp">#include <string> #include <vector> void loopover(std::vector<std::string>&vs) { for( auto i=vs.begin();;i<vs.end();i++) { } }
2.可以避免类型声明时的麻烦而且避免类型声明时的错误。事实上,在C/C++中,存在着很多隐式或者是用户自定义类型的转换规则(比如整型与字符型进行加法运算后,表达式返回整型,这是一条隐式规则)。这些规则并非容易记忆,尤其是在用户自定义很多操作符以后,这个时候auto就有用户之地了。看一下例子
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); }
上面定义了一个float类型的变量radius(半径)以及一个自定义类型PI的变量pi,在计算周长的时候,使用auto类型来定义变量circumference。这里PI在于float类型数据相乘时,其返回值为double。而PI得定义可能是在其他的地方(头文件里),main函数的程序可能就不知道PI的作者为了避免数据上溢或者是精度上的降低而返回了double类型的浮点数。因此main函数程序员如果使用float类型声明circumference,就可能会享受不了PI作者细心设计带来的好处。反之,将circumference声明为auto,则毫无问题,因为编译器已经做了最好的选择。
但是auto不能解决所有的精度问题。下面例子
#include <iostream> using namespace std; int main() { unsigned int a=4294967295;//最大的unsigned int值 unsigned int b=1; auto c=a+b; cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; cout<<"c="<<c<<endl; }
上面代码中,程序员希望通过声明变量c为auto就能解决a+b溢出的问题。而实际上由于a+b返回的依然是unsigned int的值,姑且c的类型依然被推导为unsigned int,auto并不能帮上忙。这个跟动态类型语言中数据hi自动进行拓展的特性还是不一样的。
3.在C++中其“自适应”性能够在一定程度上支持泛型的编程。
回到上面class PI的例子,这里假设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应用于模板的定义中,其"自适应"性会得到更加充分的体现。我们可以看看以下例子
template<typename T1,typename T2> double Sum(T1&t1,T2&t2) { auto a=t1+t2; return a; } int main() { int a=3; long b=5; float c=1.0f; float d=2.3f; auto e=Sum<int,long>(a,b); //e的类型被推导为long auto f=Sum<float,float>(c,d);//s的类型被推导为float }
上面中Sum模板函数接受两个参数。由于T1,T2要在模板实例化时才能确定,所以Sum中将变量s的类型声明为auto的。在函数main中我们将模板实例化时。Sum<int,long>中的s变量会被推导为long类型,而Sum<float,float>中的s变量则会被推导为float。可以看到,auto与模板一起使用时,其“自适应”特性能够加强C++中泛型的能力。
三、auto的使用注意细节
①我们可以使用valatile,pointer(*),reference(&),rvalue reference(&&) 来修饰auto
auto k = 5; auto* pK = new auto(k); auto** ppK = new auto(&k); const auto n = 6;
②用auto声明的变量必须初始化
auto m; // m should be intialized
③auto不能与其他类型组合连用
auto int p; // 这是旧auto的做法。
④函数和模板参数不能被声明为auto
void MyFunction(auto parameter){} // no auto as method argument template<auto T> // utter nonsense - not allowed void Fun(T t){}
⑤定义在堆上的变量,使用了auto的表达式必须被初始化
int* p = new auto(0); //fine int* pp = new auto(); // should be initialized auto x = new auto(); // Hmmm ... no intializer auto* y = new auto(9); // Fine. Here y is a int* auto z = new auto(9); //Fine. Here z is a int* (It is not just an int)
⑥以为auto是一个占位符,并不是一个他自己的类型,因此不能用于类型转换或其他一些操作,如sizeof和typeid
int value = 123; auto x2 = (auto)value; // no casting using auto auto x3 = static_cast<auto>(value); // same as above
⑦定义在一个auto序列的变量必须始终推导成同一类型
auto x1 = 5, x2 = 5.0, x3='r'; // This is too much....we cannot combine like this
⑧auto不能自动推导成CV-qualifiers(constant
& volatile qualifiers),除非被声明为引用类型
const int i = 99; auto j = i; // j is int, rather than const int j = 100 // Fine. As j is not constant // Now let us try to have reference auto& k = i; // Now k is const int& k = 100; // Error. k is constant // Similarly with volatile qualifer
⑨auto会退化成指向数组的指针,除非被声明为引用
int a[9]; auto j = a; cout<<typeid(j).name()<<endl; // This will print int* auto& k = a; cout<<typeid(k).name()<<endl; // This will print int [9]