深入解析decltype和decltype(auto)

iShare_爱分享發表於2024-04-12

decltype關鍵字是C++11新標準引入的關鍵字,它和關鍵字auto的功能類似,也可以自動推匯出給定表示式的型別,但它和auto的語法有些不同,auto推導的表示式放在“=”的右邊,並作為auto所定義的變數的初始值,而decltype是和表示式結合在一起,語法如下:

decltype(expr) var;

它的語法像是函式呼叫,但它不是函式呼叫而是運算子,和sizeof運算子類似,在編譯期間計算好,表示式expr不會被真正執行,因此不會產生彙編程式碼,如下的程式碼:

int func(int);
decltype(func());

func函式不會真正被呼叫,只會在編譯期間獲取他的型別。decltype和auto在功能上大部分相似,但推導規則和應用場景存在一些區別,如用auto定義變數時必須提供初始值表示式,利用初始值表示式推匯出型別並用它作為變數的初始值,而decltype定義變數時可以不需要初始值。還有使用auto作為值語義的推導時,會忽略表示式expr的引用性和CV屬性,而decltype可以保留這些屬性,關於auto的詳細解析,可以參考另一篇文章《深入解析C++的auto自動型別推導》

decltype在普通程式碼中應用並不廣泛,主要用在泛型程式設計中較多,因此沒有auto使用得多,下面將介紹decltype的推導規則,在介紹過程中遇到和auto規則不一樣的地方則將兩者對照說明,最後再介紹decltype無法被auto替代的應用場景。

推導規則

我將decltype的推導規則歸納為兩條,根據expr有沒有帶小括號分為兩種形式,如以下的形式:

decltype(expr)
// 或者
decltype((expr))
  • expr沒有帶括號的情形

當expr是單變數的識別符號、類的資料成員、函式名稱、陣列名稱時,推匯出來的結果和expr的型別一致,並且會保留引用屬性和CV修飾詞,如下面的例子:

int func(int, int) {
    int x;
    return x;
}

class Base {
public:
	int x = 0;
};

int x1 = 1;		// (1) decltype(x1)為int
const int& x2 = 2;	// (2) decltype(x2)為const int&
const Base b;				
b.x;			// (3) decltype(b.x)為int
int a[10];		// (4) decltype(a)為int[10]
decltype(func);		// (5) 結果為int(int, int)

(1)式decltype(x1)的結果和x1的型別一致,為int型別。

(2)式的結果也是和x2一致,這裡和auto的推導規則不同的是,它可以保留x2的引用屬性和const修飾詞,所以它的型別是const int&。

(3)式中定義的類物件b雖然是const的,但成員x的型別是int型別,所以結果也是int。

(4)和(5)都保留了原本的型別,這個也是和auto的推導結果不同的,使用auto推導的規則它們會退化為指標型別,這裡則保留了它們陣列和函式的型別。

當expr是一條表示式時,decltype(expr)的結果視expr表示式運算後的結果而定(在編譯時運算而非執行時運算),當expr返回的結果是右值時,推導的結果和返回結果的型別一致,當expr返回的結果是左值時,推導的結果是一個引用,見下面的例子:

int x1 = 1;
int x2 = 2;
decltype(x1 + x2);	// (1) int
decltype(func());	// (2) int
decltype(x1,x2);	// (3) int&
decltype(x1,0);		// (4) int
decltype(a[1]);		// (5) int&

(1)式因為兩個變數相加後返回一個數值,它是一個右值,所以推導結果和它的型別一致,這裡換成加減乘除都是一樣的。

(2)是一個函式呼叫,跟上面的使用函式名稱不同,這裡會呼叫函式(編譯時),根據函式的返回結果來確定推匯出來的型別,如果返回結果是引用或者指標型別,則推導結果也會引用或者指標型別,此函式返回的結果是int型,所以結果也是int型。

(3)和(4)是逗號表示式,它的返回結果是逗號後的那個語句,(3)是返回x2,它是一個變數,是一個左值,所以推導結果是int&,而(4)的返回結果是0,是一個右值,因此結果和它的型別一致。

(5)是訪問陣列中的元素,它是一個左值,因此推導結果是一個引用。

  • expr帶括號的情形

當expr帶上括號之後,它的推導規則有了變化,表示式加上括號後相當於去執行這條語句然後根據返回結果的型別來推導,見下面的例子:

class Base {
public:
	int x = 0;
};

int x1 = 1;
int x2 = 2;
const Base b;
b.x;
decltype((x1+x2)); 	// (1) int
decltype((x1));		// (2) int&
decltype((b.x));	// (3) const int&

(1)式中相加後的結果是一個右值,加上括號後依然是一個右值,因此推導結果是int。

(2)式中跟之前沒有加括號的情況不一樣,加上括號相當於是返回x1變數,因此是一個左值,推導結果是一個引用。

(3)式中也跟之前的結果不一樣了,加上括號相當於返回類的資料成員x,因此是一個左值,推導結果是一個引用,但因為定義的類物件b是一個const物件,要保持它的內容不可被修改,因此引用要加上const修飾。

最後還有要注意一點的是,decltype和auto一樣也可以和&和一起結合使用,但和auto的規則不一樣,auto與&和結合表示定義的變數的型別是一個引用或者指標型別,而decltype則是保留這個符號並且和推導結果一起作為最終的型別,見下面的例子:

int x1 = 1;
auto *pi = &x1;		// (1) auto為int,pi為int*
decltype(&x1) *pp;	// (2) decltype(&x1)為int*,pp為int**

(1)式中的auto推導結果為int而不是int,要將pi定義為指標型別需要明確寫出auto

(2)式的decltype(&x1)的推導結果為int,它會和定義中的(*pp前面的星號)結合在一起,因此最終的結果是int**。

decltype的使用場景

上面提到decltype和auto的一個區別就是使用auto必須要有一個初始值,而decltype在定義變數時可以不需要初始值,這在定義變數時暫時無法給出初始值的情況下非常有用,見下面的例子:

#include <map>
#include <string>

template<typename ContainerT>
class Object {
public:
    void init(ContainerT& c) { it_ = c.begin(); }
private:
    decltype(ContainerT().begin()) it_;
};

int main() {
    std::map<std::string, int> m;
    Object<std::map<std::string, int>> obj;
    obj.init(m);
}

在定義類的成員it_時還沒有初始值,這時無法使用auto來推導它的型別,況且這裡也無法使用auto來定義類的資料成員,因為現在還不支援使用auto來定義非靜態的資料成員的,但使用decltype卻是可以的。

還有一種情形是使用auto無法做到的,就是auto在使用值語義的推導規則的時候會忽略掉引用屬性和CV修飾詞,比如:

int i = 1;
const int& j = i;
auto x = j;	// auto的結果為int

這裡x無法推匯出和變數j一樣的型別,你可能會說,如果要使用引用型別,那可以這樣寫:

const auto& x = j;	// auto的結果為int, x的型別const int&

但這又會帶來其它的問題,這樣定義出來的變數的型別永遠都是const引用的型別,無法做到根據不同的表示式推匯出相應的型別,如果使用decltype則可以做到:

int i = 1;
const int& j = i;
decltype(j) x = j;	// x的型別為const int&
decltype(i) y = i;	// y的型別為int

上面的程式碼使用decltype就可以根據不同的初始值表示式來推匯出不同的結果。但你可能會覺得初始值表示式要在左右兩邊寫上兩遍,比較累贅,單個變數的還好,如果是個長表示式的話就會顯得程式碼很冗餘,也不優雅,比如:

int x = 1;
int y = 2;
double z = 5.0;
decltype(x + y + z) i = x + y + z;

如果上面的例子中表示式再長點就更難看也更麻煩了,幸好C++14標準提出了decltype和auto結合的功能,也就是decltype(auto)的用法。

decltype(auto)的使用解析

自動推導表示式的結果的型別

decltype(auto)的使用語法規則如下:

decltype(auto) var = expr;

它的意思是定義一個變數var,auto作為型別佔位符,使用自動型別推導,但推導的規則是按照decltype的規則來推導。因此上面的程式碼可以這樣來寫:

decltype(auto) j = x + y + z;

它的用法跟使用auto一樣,利用右邊的表示式來推匯出變數j的型別,但是推導規則使用的是decltype的規則。這對需要保持右邊表示式的引用屬性和CV修飾詞時就非常有用,上面的程式碼可以改為:

int i = 1;
const int& j = i;
decltype(auto) x = j;	// x的型別為const int&
decltype(auto) y = i;	// y的型別為int

decltype(auto)用於推導函式返回值的型別

decltype(auto)可以用於推導函式返回值的型別,auto也可以用於推導函式的返回值型別,在講解auto的那篇文章中就已講解過。但auto有個問題就是會忽略掉返回值的引用屬性,但如果你用auto&來推導返回值型別的話,那所有的型別都將是引用型別,這也不是實際想要的效果,有沒有辦法做到如果返回值型別是值型別時就推匯出值型別,如果返回值型別是引用則推匯出結果是引用型別?假設有一個處理容器元素的函式,它接受一個容器的引用和一個索引,函式處理完這個索引的元素之後再返回這個元素,一般來說,容器都有過載了“[]"運算子,但有的容器可能返回的是這個元素的值,有的可能返回的是元素的引用,如:

T& operator[](std::size_t index);
// 或者
T operator[](std::size_t index);

這時我們就可以用decltype(auto)來自動推導這個函式的返回值型別,函式的定義如下:

template<typename Container, typename Index>
decltype(auto) process(Container& c, Index i) {
    // processing
    return c[i];
}

當傳進來的容器的operator[]函式返回的是引用時,則上面的函式返回的是引用型別,如果operator[]函式返回的是一個值時,則上面的函式返回的是這個值的型別。

decltype(auto)使用陷阱

最後,對於decltype(auto)能夠推導函式返回值為引用型別這一點,需要提醒一下的是,小心會有下面的陷阱,如下面的函式:

decltype(auto) func() {
    int x;
    // do something...
    return x;
}

這裡推匯出來的返回值型別是int,並且會複製區域性變數x的值,這個沒有問題。但如果是這樣的定義:

decltype(auto) func() {
    int x;
    // do something...
    return (x);
}

這個版本返回的是一個引用,它將引用到一個即將銷燬的區域性變數上,當這個函式返回後,所返回的引用將引用到一個不存在的變數上,造成引用空懸的問題,程式的結果將是未知的。無論是有意的還是無意的返回一個引用,都要特別小心。

此篇文章同步釋出於我的微信公眾號:深入解析decltype和decltype(auto)

如果您感興趣這方面的內容,請在微信上搜尋公眾號iShare愛分享或者微訊號iTechShare並關注,或者掃描以下二維碼關注,以便在內容更新時直接向您推送。
image

相關文章