系列文章

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 运算符用来获取一个表达式的类型信息。需要包含<typeinfo>头文件才可以使用。
主要使用分为俩种场景:

  1. 对于基本类型(intfloatC++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
  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的类型是确定不了的。所以才引入了类型推导。最终定为了autodecltype,但是俩者功能并不相同。

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)的参数作为出参。这个出参的类型是根据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;
}

编译Sum(a,b,e)时,因为入参的类型是intlong 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_referenceis_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限定符的继承与冗余的符号

在定义变量/对象有被constvolatile限定符修饰,在使用decltype进行推导时,其成员不会继承constvolatile限定符。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后的*号,并不会被编译器忽略。

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

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐