天天看點

C++11之decltype類型推導(使用場景、推導四規則、cv限定符)系列文章typeid與decltypedecltype的使用場景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的使用場景
    • 增加代碼的可讀性、簡潔性
    • 擷取匿名自定義類型的類型
    • 用于泛式程式設計
    • 推導函數的傳回值類型
  • decltype推導四規則
    • 執行個體1
    • 執行個體2
    • 使用模闆來輔助推導識别
  • cv限定符的繼承與備援的符号

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;
}
           

運作結果:

擷取匿名自定義類型的類型

在一般情況下,我們定義的匿名枚舉類型、匿名聯合體/共用體、匿名結構體,是無法二次建立變量的。但是有了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;
}

           

運作結果:

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

中的泛型能力提升到更高的水準。

繼續閱讀