C++11之decltype类型推导(使用场景、推导四规则、cv限定符)
`decltype`算得上是`C++11`中类型推导使用方式上最灵活的一种。虽然看起来它的推导规则比较复杂,有的时候跟`auto`推导结果还略有不同,但大多数时候,我们发现使用`decltype`还是自然而亲切的。一些细则的区别,读者可以在使用时遇到问题再返回查验。而下面的追踪返回类型的函数定义,则将融合`auto`、`decltype`,将`C++11`中的泛型能力提升到更高的水平。
系列文章
C++11之正则表达式(regex_match、regex_search、regex_replace)
C++11之线程库(Thread、Mutex、atomic、lock_guard、同步)
C++11之智能指针(unique_ptr、shared_ptr、weak_ptr、auto_ptr)浅谈内存管理
C++11之强制类型转换(static_cast,const_cast,dynamic_cast,reinterpret_cast)
C++11之右值引用:移动语义和完美转发(带你了解移动构造函数、纯右值、将亡值、右值引用、std::move、forward等新概念)
C++11之内联名字空间(inline namespace)和ADL特性(Argument-Dependent name Lookup)
目录
typeid与decltype
在学习decltype之前,我们先了解一下typeid
运算符。typeid
运算符用来获取一个表达式的类型信息。需要包含<typeinfo>
头文件才可以使用。
主要使用分为俩种场景:
- 对于基本类型(
int
、float
等C++
内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。 - 对于类类型的数据(对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
我们可以通过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)
- 如果e是一个没有带括号的**标记符表达式(id-expression)**或者类成员访问表达式,那么
decltype(e)
就是e
所命名实体的类型。此外,如果e
是一个被重载的函数,则会导致编译时错误。 - 否则,假设e的类型是T。如果e是一个将亡值(xvalue),那么decltype(e)为T&&(右值引用)。
- 否则,假设e的类型是T。如果e是一个左值,则decltype(e)为T&(左值引用)。
- 否则,假设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
中的泛型能力提升到更高的水平。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)