C++ auto和decltype
C++11新标准引入了auto和decltype这两种类型说明符。它们的共同之处在于不需要显式声明变量的类型,而是让编译器去推断变量类型。但是,它们之间又有一些细微的差异。下面就让我们一起深入理解这两个类型说明符。
auto
因为auto是根据表达式的类型来推断变量的类型,所以auto定义的变量必须要有初始值,如:
auto val1 = 2; //val1为int型
auto val2 = 'a'; //val2为char型
auto val3 = val1 + val2; //val2隐式转换为int型,所以val3为int型
auto *val4 = &val3; //val4为int*型
以上的例子似乎让我们认为,auto确定的类型和初始值的类型完全一样。其实不是的,让我们看一些其他的例子。
当auto遇上引用时:
int i = 0; //i为int型变量
const int &i1 = i; //i1为对非常量的常量引用
const int j = 0; //j为int型常量
const int &j1 = j; //j1为对常量的常量引用
auto val5 = j; //val5为int型
auto val6 = i1; //val6为int型
auto val7 = j1; //val7为int型
首先我们需要理解常量引用这个概念,其实严格来讲,并不存在常量引用。因为引用不是一个对象,所以不可能让引用本身恒定不变。那么,我们为什么要使用“常量引用”这个说法呢,其实更多的是为了表达这种引用关系,即通过该引用不能改变被引用的对象。了解这个,也就理解了上述的“对非常量的常量引用”和“对常量的常量引用”。为了使文字不过于使人迷惑,我们之后都称呼“被引用对象”为原始对象。
接下来,因为引用又可以当做被引用对象的别名。所以无论引用是常量引用(有const)还是非常量引用(无const),最后都会演变为通过推断原始对象的类型来定义变量:即编译器以原始对象的类型作为auto类型。
auto val6 = i1;
—–(变成)—>auto val6 = i;
很好理解val6为int型。auto val7 = j1;
—–(变成)—>auto val7 = j;
,所以val7和val5类型一样。
关于val5和val7的类型,就不得不说auto的一个很重要的性质,就是当设置auto类型的变量(非引用),auto会忽略初始值的顶层const,保留底层const。
对于非指针变量(非引用)而言,只可能存在顶层const。当变量为常量,则该变量存在顶层const;若变量为非常量,则该变量不存在顶层const。
对于引用而言,该引用为常量引用时,存在顶层const;反之不存在。指针引用则有可能出现底层const。
对于指针的顶层/底层const之后会详细介绍。
因为j为常量,存在顶层const,所以auto忽略j的顶层const,所以val7和val5均为int型。如果想要使用auto定义常量对象,则要显示声明,const auto val8 = j;
这样val8就是int型常量。
另外当设置auto类型的引用时,auto会保留初始值的顶层const。
当auto &val9 = i1;
时,此时i1的顶层const属性被保留,所以val9为常量引用。其实也很好理解,如果对于一个常量引用x,之后绑定在x上的引用x1,x2…如果不是常量引用,那不就能通过x1,x2等来改变x引用的对象,这显然是不合理的,C++也不会允许这样的漏洞。
当auto遇上指针时:
首先我们需要理解之前提到的顶层const和底层const的概念,对于指针而言:
- 顶层const表示指针本身是个常量。
- 底层const表示指针所指的对象是个常量。
const int i = 20; //i为整型常量
const int *p1 = &i; //p1为指向常量的指针,具有底层const
const int *const p2 = &i; //p2为指向常量的常量指针,具有顶层const和底层const
我们直接来看代码:
auto p3 = p1; //保留底层const,所以p3类型为const int*
auto p4 = p2; //保留底层const,忽略顶层const,所以p4类型为const int*
auto p5 = &i; //&i为const int*型,所以保留底层const,p5类型为const int*
相信根据上述的保留底层const,忽略顶层const性质,大家都能很好的理解。
decltype
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。相对于auto的不完全一致,decltype则是完全对应与表达式结果的类型。
这种一致性体现在,如果表达式的结果对象能作为一条赋值语句的左值,那么这个表达式将向decltype返回一个引用类型。
int i = 42, *p = &i, &r = i;
decltype(r + 0) v1; //r + 0的结果为int型,所以v1为int型
decltype(*p) v2 = i; //*p为左值,所以v2为int&
decltype(r) v3 = i; //r为int&,为左值,所以v3为int&
要注意的是:decltype对于变量是否加括号有着不同的处理:
decltype(i) v4; //i不加括号,v4为i的类型,即int
decltype((i)) v5 = i; //i加括号,将其当成表达式,变量是一种可以可以作为左值的特殊表达式,所以v5为int&
总结
关于auto类型的引用和指针,再多说一些。由于引用和指针都与初始值相关。可以想象,初始值绑定引用并衍生指针。所以如果初始值是常量,那么后续的引用和指针都应该是绑定常量或指向常量。