4.3.3 decltype推导四规则
作为auto的伙伴,decltype在C++11中也非常重要。不过跟auto一样,由于应用广泛,所以使用decltype也有很多的细则条款需要注意。很多时候,用户会发现decltype的行为并不如预期,那么下面的规则可能会更好地解释这些“不如预期”的编译器行为。
大多数时候,decltype的使用看起来非常平易近人,但是有时我们也会落入一些令人疑惑的陷阱。最典型的就是代码清单4-24所示的这个例子。
代码清单4-24
int i;
decltype(i)a;//a:int
decltype((i))b;//b:int&,无法编译通过
//编译选项:g++ -std=c++11 4-3-9.cpp
我们在编译代码清单4-24的时候,会惊奇地发现,decltype((i))b;这样的语句编译不过。编译器会提示b是一个引用,但没有被赋初值。而decltype(i)a;这一句却能通过编译,因为其类型被如预期地推导为int。
这种问题显得非常诡异,单单多了一对圆括号,decltype所推导出的类型居然发生了变化。事实上,C++11中decltype推导返回类型的规则比我们想象的复杂。具体地,当程序员用decltype(e)来获取类型时,编译器将依序判断以下四规则:
1)如果e是一个没有带括号的标记符表达式(id-expression)或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译时错误。
2)否则,假设e的类型是T,如果e是一个将亡值(xvalue),那么decltype(e)为T&&。
3)否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T&。
4)否则,假设e的类型是T,则decltype(e)为T。
这里我们要解释一下标记符表达式(id-expression)。基本上,所有除去关键字、字面量等编译器需要使用的标记之外的程序员自定义的标记(token)都可以是标记符(identifier)。而单个标记符对应的表达式就是标记符表达式。比如程序员定义了:
int arr[4];
那么arr是一个标记符表达式,而arr[3]+0,arr[3]等,则都不是标记符表达式。
我们再回到代码清单4-24,并结合decltype类型推导的规则,就可以知道,decltype(i)a;使用了推导规则1——因为i是一个标记符表达式,所以类型被推导为int。而decltype((i))b;中,由于(i)不是一个标记符表达式,但却是一个左值表达式(可以有具名的地址),因此,按照decltype推导规则3,其类型应该是一个int的引用。
上面的规则看起来非常复杂,但事实上,在实际应用中,decltype类型推导规则中最容易引起迷惑的只有规则1和规则3。我们可以通过代码清单4-25所示的这个例子再加深一下理解。
代码清单4-25
int i=4;
int arr[5]={0};
int*ptr=arr;
struct S{double d;}s;
void Overloaded(int);
void Overloaded(char);//重载的函数
int&&RvalRef();
const bool Func(int);
//规则1:单个标记符表达式以及访问类成员,推导为本类型
decltype(arr)var1;//int[5],标记符表达式
decltype(ptr)var2;//int*,标记符表达式
decltype(s.d)var4;//double,成员访问表达式
decltype(Overloaded)var5;//无法通过编译,是个重载的函数
//规则2:将亡值,推导为类型的右值引用
decltype(RvalRef())var6=1;//int&&
//规则3:左值,推导为类型的引用
decltype(true?i:i)var7=i;//int&,三元运算符,这里返回一个i的左值
decltype((i))var8=i;//int&,带圆括号的左值
decltype(++i)var9=i;//int&,++i返回i的左值
decltype(arr[3])var10=i;//int&[]操作返回左值
decltype(ptr)var11=i;//int&操作返回左值
decltype("lval")var12="lval";//const char(&)[9],字符串字面常量为左值
//规则4:以上都不是,推导为本类型
decltype(1)var13;//int,除字符串外字面常量为右值
decltype(i++)var14;//int,i++返回右值
decltype((Func(1)))var15;//const bool,圆括号可以忽略
//编译选项:g++ -std=c++11-c 4-3-10.cpp
代码清单4-25中我们将四种规则的例子都列了出来。可以看到,规则1不但适用于基本数据类型,还适用于指针、数组、结构体,甚至函数类型的推导,事实上,规则1在decltyp类型推导中运用的最为广泛。而规则2则比较简单,基本上符合程序员的想象。
规则3其实是一个左值规则。decltype的参数不是标志表达式或者类成员访问表达式,且参数都为左值,推导出的类型均为左值引用。规则4则是适用于以上都不适用者。我们这里看到了++i和i++在左右值上的区别,以及字符串字面常量lval、非字符串字面常量1在左右值间的区别。
看过这么多规则,读者可能觉得过于复杂,但事实上,如同我们之前提到的,引起麻烦的只是规则3带来的左值引用的推导。一个简单的能够让编译器提示的方法是,如果使用decltype定义变量,那么先声明这个变量,再在其他语句里对其进行初始化。这样一来,由于左值引用总是需要初始化的,编译器会报错提示。另外一些时候,C++11标准库中添加的模板类is_lvalue_reference,可以帮助程序员进行一些推导结果的识别。我们看看代码清单4-26所示的例子。
代码清单4-26
include <type_traits>
include <iostream>
using namespace std;
int i=4;
int arr[5]={0};
int*ptr=arr;
int&&RvalRef();
int main(){
cout<<is_rvalue_reference<decltype(RvalRef())>::value<<endl;//1
cout<<is_lvalue_reference<decltype(true?i:i)>::value<<endl;//1
cout<<is_lvalue_reference<decltype((i))>::value<<endl;//1
cout<<is_lvalue_reference<decltype(++i)>::value<<endl;//1
cout<<is_lvalue_reference<decltype(arr[3])>::value<<endl;//1
cout<<is_lvalue_reference<decltype(*ptr)>::value<<endl;//1
cout<<is_lvalue_reference<decltype("lval")>::value<<endl;//1
cout<<is_lvalue_reference<decltype(i++)>::value<<endl;//0
cout<<is_rvalue_reference<decltype(i++)>::value<<endl;//0
}
//编译选项:g++ -std=c++11 4-3-11.cpp
代码清单4-26中,我们使用了模板类is_lvalue_reference的成员value来查看decltype的效果(1表示是左值引用,0则反之)。如我们所见,代码清单4-26中凡是符合规则3的,都会被推导为左值引用。如果程序员在程序的书写中不是非常确定decltype是否将类型推导为左值引用,也可以通过这样的小实验来辅助确定。这里我们还使用了模板函数is_rvalue_reference,同样,程序员也可以通过它来确定decltype是否推导出了右值引用。