typeid与decltype
在学习decltype之前,我们先了解一下typeid运算符。typeid 运算符用来获取一个表达式的类型信息。需要包含<typeinfo>头文件才可以使用。
主要使用分为俩种场景:
1.对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
2.对于类类型的数据(对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
我们可以通过name
方法获取到这个类型,通过hash_code
方法返回该类型唯一的哈希值(值得注意的是hash_code
是在运行时获取的信息,hash_code
也是C++11
新加入的)。下面就通过hash_code
方法判断了俩个变量的类型是否一致。
#include <iostream> #include <typeinfo> using namespace std; class A{}; class B{}; int main() { A a; B b; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; A c; bool ret = (typeid(a).hash_code() == typeid(b).hash_code()); bool ret2 = (typeid(a).hash_code() == typeid(c).hash_code()); cout << "Same type?" << endl; cout << "A and B?" << boolalpha << ret << endl; cout << "A and C?" << boolalpha << ret2 << endl; return 0; }
运行结果:
class A class B Same type? A and B?false A and C?true
类型推导是用于模板编程和泛式编程中的。因为在其他编程中,各类型的确定的,不需要类型推导。而在泛式编程中,类型就是未知的,例如下面这个例子,在编译期T的类型是确定不了的。所以才引入了类型推导。最终定为了auto
和decltype
,但是俩者功能并不相同。
template<typename T> void test(T t) { ; }
decltype
的使用是非常简单的。语法:decltype(type) 变量名;
#include <typeinfo> #include <iostream> using namespace std; int main() { int i = 1; decltype(i) j = 0; cout << typeid(j).name() << endl; float a; double d; decltype(a + d) c; cout << typeid(c).name() << endl; return 0; }
运行结果:
int double
deletype以一个表达式或者变量为参数,然后返回该表达式/变量的类型,是一个类型指示符。decltype类型推导和auto自动类型一样都是编译期确定。
decltype的使用场景
增加代码的可读性、简洁性
在使用迭代器时,每次都需要很长的迭代器类型,例如map<int,int>::iterator
,使用decltype就可以将类型进行重定义,简化代码,提高可读性。
#include <iostream> #include <vector> using namespace std; int main() { vector<int> arr(10); typedef decltype(arr.begin()) vecIter; for(vecIter i = arr.begin(); i < arr.end(); ++i) { *i = 1; } for (decltype(arr)::iterator i = arr.begin(); i < arr.end(); ++i) { cout << *i << " "; } cout << endl; return 0; }
运行结果:
1 1 1 1 1 1 1 1 1 1
获取匿名自定义类型的类型
在一般情况下,我们定义的匿名枚举类型、匿名联合体/共用体、匿名结构体,是无法二次创建变量的。但是有了decltype就可以实现上述的功能。
下面通过decltype类型指示符通过匿名结构体数组变量推导出对应的匿名枚举类型。
#include <iostream> enum // 匿名枚举 { A, B, C, }test; union // 匿名联合体/共用体 { decltype(test) key; char* name; }test2; struct // 匿名结构体数组 { int d; decltype(test2) id; }test3[10]; int main() { decltype(test3) stu; stu[0].id.key = decltype(test)::A; // 引用匿名强类型枚举的值 return 0; }
用于泛式编程
见下面这个例子,Sum
函数模板增加了一个类型为decltype(t1 + t2)
的参数作为出参。这个出参的类型是根据T1
和T2
入参类型共同决定的。如果入参的类型是数组的话就需要为数组提供特殊的重载版本。
template<typename T1, typename T2> void Sum(T1& t1, T2& t2, decltype(t1 + t2)& s) { s = t1 + t2; } void Sum(int a[], int b[], int c[]) { // 数组版本 } int main() { int a = 34; long int b = 5; float c = 1.0f; float d = 2.3f; long int e = 0; float f = 0; Sum(a, b, e); Sum(c, d, f); int arr1[5]; int arr2[5]; int arr3[5]; Sum(arr1, arr2, arr3); return 0; }
编译Sum(a,b,e)
时,因为入参的类型是int
和 long int
所以,运算后出参类型就是long int
。其他同理。
推导函数的返回值类型
基于decltype
的模板类result_of
,可以推导函数的返回值。
#include <type_traits> using namespace std; typedef double (*func)(); int main() { result_of<func()>::type f = 1; // 推导函数的返回值 std::cout << typeid(f).name() << std::endl; return 0; }
运行结果:
double
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[2]; 这里的arr就是标记符表达式,而arr[1]、arr[1] + 1都不是标记符表达式。
实例1
了解了推导规则后,我们来练习一个吧。
int main() { int i; decltype(i) a; // a type: int decltype((i)) b; // b type: int& 编译失败 return 0; }
先来分析decltype(i) a;
:i
是一个标记符表达式,所以推导的类型就是实体的类型,也就是int
。
再来分析decltype((i)) b;
:(i)
不是一个标记符表达式,所以第一条不成立,(i)
是一个左值,所以符合规则第三条,那么推导的类型为int&
。
实例2
经过前面的简单练习,我相信应该对这个规则有一定的认知了。那么我们就在分析一个稍微复杂的程序。
定义下面变量以及函数,用于之后的分析:
int i = 4; int arr[5] = { 0 }; int* ptr = arr; struct S { double d; S():d(0){} }s; void Test(); // 重载 void Overloaded(int); void Overloaded(char); int&& RvalRef(); const bool Func(int);
规则1 单个标记符表达式以及访问类成员变量 均为推导为原本类型
推导 | 说明 |
decltype(arr) var1; | int[5] 标记符表达式 |
decltype(ptr) var2; | int& 标记符表达式 |
decltype(Test) var3; | void __cdecl(void) 标记符表达式 |
decltype(s.d) var4; | double 成员访问表达式 |
decltype(Overloaded) var5; | 编译失败 不支持重载版本的函数 |
规则2 将亡值 推导为类型的右值引用 &&
推导 | 说明 |
decltype(RvalRef()) var6 = 1; | int&& |
规则3 左值 推导为类型的引用
推导 | 说明 |
decltype(true ? i : 11) var7 = i; | int& 三目运算符 这里会返回一个int类型 |
decltype((i)) var8 = i; | int& 带括号的左值 |
decltype(++i) var9 = i; | int& ++i 返回i的左值 |
decltype(arr[3]) var10 = i; | int& []下标运算符会返回左值 |
decltype(*ptr) var11 = i; | int& *ptr <=> arr[0] 返回左值 |
decltype(“lval”) var12 = “lval”; | const char(&)[5] 字符串字面常量 属于左值 |
规则4 以上都不是,就推导为自身类型
推导 | 说明 |
decltype(1) var13; | int 除了字符串外的字面常量均为右值 |
decltype(i++) var14; | int i++返回右值 |
decltype(Func(i)) var15; | const bool 返回右值 |
使用模板来辅助推导识别
我们可以通过C++11的is_lvalue_reference
、is_rvalue_reference
,可以帮助我们对一些推导结果的识别。
#include <type_traits> #include <iostream> int main() { int i = 4; int arr[5] = { 0 }; int* ptr = arr; int&& RvalRef(); // 是右值引用吗? std::cout << std::is_rvalue_reference<decltype(RvalRef())>::value << std::endl; std::cout << std::is_rvalue_reference<decltype(i++)>::value << std::endl; // 是左值引用吗? std::cout << std::is_lvalue_reference<decltype(true ? i : i)>::value << std::endl; std::cout << std::is_lvalue_reference<decltype((i))>::value << std::endl; std::cout << std::is_lvalue_reference<decltype(i)>::value << std::endl; std::cout << std::is_lvalue_reference<decltype(++i)>::value << std::endl; std::cout << std::is_lvalue_reference<decltype(arr[0])>::value << std::endl; std::cout << std::is_lvalue_reference<decltype(*ptr)>::value << std::endl; std::cout << std::is_lvalue_reference<decltype("lval")>::value << std::endl; std::cout << std::is_lvalue_reference<decltype(i++)>::value << std::endl; return 0; }
cv限定符的继承与冗余的符号
在定义变量/对象有被const和volatile限定符修饰,在使用decltype进行推导时,其成员不会继承const和volatile限定符。is_const用来判断是否被const限定符修饰,is_volatile用来判断是否被volatile修饰。
#include <type_traits> #include <iostream> using std::cout; using std::endl; using std::is_const; using std::is_volatile; int main() { const int c = 0; volatile int v = 0; struct S { int i; }; const S a = { 0 }; volatile S b; volatile S* p = &b; cout << is_const<decltype(c)>::value << endl; // 1 cout << is_volatile<decltype(v)>::value << endl; //1 cout << is_const<decltype(a)>::value << endl;//1 cout << is_volatile<decltype(b)>::value << endl;//1 cout << is_const<decltype(a.i)>::value << endl;//0 cout << is_volatile<decltype(p->i)>::value << endl;//0 return 0; }
decltype在表达式的推导中,如果遇到一个冗余的符号将会被优化掉。
#include <type_traits> #include <iostream> using std::cout; using std::endl; using std::is_lvalue_reference; using std::is_rvalue_reference; int main() { int i = 1; int& j = i; int* p = &i; const int k = 1; decltype(i)& var1 = i; // 左值引用 decltype(j)& var2 = i; // 冗余的& 将会被优化掉 左值引用 cout << is_lvalue_reference<decltype(var1)>::value << endl; // 1 cout << is_rvalue_reference<decltype(var2)>::value << endl; // 0 cout << is_lvalue_reference<decltype(var2)>::value << endl; // 1 decltype(p)* var3 = &i; // 编译失败 decltype(p)* var4 = &p; // int** auto* v3 = p; // int* v3 = &i; const decltype(k) var5 = 1; // const冗余 将会被优化掉 return 0; }
这里特别要注意的是decltype(p)*的情况。可以看到,在定义var4变量的时候,由于p的类型是int*,因此var3被定义为了int**类型。这跟auto声明中,·也可以是冗余的不同。在decltype后的*号,并不会被编译器忽略。
此外我们也可以看到,var4中 const可以被冗余的声明,这就会被优化掉,同样的volatilc限制符也会如此。
总的说来,decltype算得上是C++11中类型推导使用方式上最灵活的一种。虽然看起来它的推导规则比较复杂,有的时候跟auto推导结果还略有不同,但大多数时候,我们发现使用decltype还是自然而亲切的。一些细则的区别,读者可以在使用时遇到问题再返回查验。而下面的追踪返回类型的函数定义,则将融合auto、decltype,将C++11中的泛型能力提升到更高的水平。