• C++11之decltype类型推导(使用场景、推导四规则、cv限定符)


    系列文章

    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之Lanbda表达式(匿名函数)

    C++11之右值引用:移动语义和完美转发(带你了解移动构造函数、纯右值、将亡值、右值引用、std::move、forward等新概念)

    C++11之委派构造函数

    C++11之内联名字空间(inline namespace)和ADL特性(Argument-Dependent name Lookup)

    C++11之模板的别名

    C++11之一般化的SFINAE规则

    C++11之auto类型推导



    typeid与decltype

    在学习decltype之前,我们先了解一下typeid运算符。typeid 运算符用来获取一个表达式的类型信息。需要包含头文件才可以使用。
    主要使用分为俩种场景:

    1. 对于基本类型(intfloatC++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
    2. 对于类型的数据(对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。

    我们可以通过name方法获取到这个类型,通过hash_code方法返回该类型唯一的哈希值(值得注意的是hash_code是在运行时获取的信息,hash_code也是C++11新加入的)。下面就通过hash_code方法判断了俩个变量的类型是否一致。

    #include 
    #include 
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    运行结果:

    class A
    class B
    Same type?
    A and B?false
    A and C?true
    
    • 1
    • 2
    • 3
    • 4
    • 5

    类型推导是用于模板编程和泛式编程中的。因为在其他编程中,各类型的确定的,不需要类型推导。而在泛式编程中,类型就是未知的,例如下面这个例子,在编译期T的类型是确定不了的。所以才引入了类型推导。最终定为了autodecltype,但是俩者功能并不相同。

    template<typename  T>
    void test(T t)
    {
    	;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    decltype的使用是非常简单的。语法:decltype(type) 变量名;

    #include 
    #include 
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行结果:

    int
    double
    
    • 1
    • 2

    deletype以一个表达式或者变量为参数,然后返回该表达式/变量的类型,是一个类型指示符。decltype类型推导和auto自动类型一样都是编译期确定。

    decltype的使用场景

    增加代码的可读性、简洁性

    在使用迭代器时,每次都需要很长的迭代器类型,例如map::iterator ,使用decltype就可以将类型进行重定义,简化代码,提高可读性。

    #include 
    #include 
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果:

    1 1 1 1 1 1 1 1 1 1
    
    • 1

    获取匿名自定义类型的类型

    在一般情况下,我们定义的匿名枚举类型、匿名联合体/共用体、匿名结构体,是无法二次创建变量的。但是有了decltype就可以实现上述的功能。

    下面通过decltype类型指示符通过匿名结构体数组变量推导出对应的匿名枚举类型。

    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    用于泛式编程

    见下面这个例子,Sum函数模板增加了一个类型为decltype(t1 + t2)的参数作为出参。这个出参的类型是根据T1T2入参类型共同决定的。如果入参的类型是数组的话就需要为数组提供特殊的重载版本。

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    编译Sum(a,b,e)时,因为入参的类型是intlong int 所以,运算后出参类型就是long int。其他同理。

    推导函数的返回值类型

    基于decltype的模板类result_of,可以推导函数的返回值。

    #include 
    
    using namespace std;
    
    typedef double (*func)();
    
    int main()
    {
    	result_of<func()>::type f = 1; // 推导函数的返回值
    	std::cout << typeid(f).name() << std::endl;
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    double
    
    • 1

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    先来分析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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    规则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_referenceis_rvalue_reference,可以帮助我们对一些推导结果的识别。

    #include 
    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    cv限定符的继承与冗余的符号

    在定义变量/对象有被constvolatile限定符修饰,在使用decltype进行推导时,其成员不会继承constvolatile限定符。is_const用来判断是否被const限定符修饰,is_volatile用来判断是否被volatile修饰。

    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    decltype在表达式的推导中,如果遇到一个冗余的符号将会被优化掉。

    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这里特别要注意的是decltype(p)*的情况。可以看到,在定义var4变量的时候,由于p的类型是int*,因此var3被定义为了int**类型。这跟auto声明中,·也可以是冗余的不同。在decltype后的*号,并不会被编译器忽略。

    此外我们也可以看到,var4const可以被冗余的声明,这就会被优化掉,同样的volatilc限制符也会如此。

    总的说来,decltype算得上是C++11中类型推导使用方式上最灵活的一种。虽然看起来它的推导规则比较复杂,有的时候跟auto推导结果还略有不同,但大多数时候,我们发现使用decltype还是自然而亲切的。一些细则的区别,读者可以在使用时遇到问题再返回查验。而下面的追踪返回类型的函数定义,则将融合autodecltype,将C++11中的泛型能力提升到更高的水平。

  • 相关阅读:
    设计模式 - 命令模式
    docker中已创建容器的修改方法
    Oracle11gr2 + plsql 配置
    Flink Window&Time 原理
    day02 真正的高并发还得看IO多路复用
    【MySQL】MySQL 数据库锁知识 讲解
    Qt中使用QNetworkAccessManager类发送https请求时状态码返回0
    基于go-micro微服务的实战-Gateway网关层的限流降级(八)
    【Java】异常
    MIGraphX推理框架第八章-动态Shape
  • 原文地址:https://blog.csdn.net/qq_45254369/article/details/127372898