decltype關鍵字是C++11新標準引入的關鍵字,它和關鍵字auto的功能類似,也可以自動推導出給定表達式的類型,但它和auto的語法有些不同,這篇文章講解了decltype的使用場景以及和auto不同的地方,同時也講解了和auto結合使用的用法。 ...
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並關註,或者掃描以下二維碼關註,以便在內容更新時直接向您推送。