詳解C++17中的decltype類型推導(dǎo)
引子
在編程過程中,有時(shí)我們需要根據(jù)表達(dá)式的類型來聲明變量,尤其是在涉及模板編程和泛型編程時(shí),經(jīng)常會遇到這樣的問題:(1)、有些泛型類型由模板參數(shù)決定,但是卻很難或根本無法表示;(2)、需要在編譯時(shí)確定變量的類型。
除此之外,我們知道auto在自動類型推導(dǎo)時(shí),會忽略類型的修飾符。如此會導(dǎo)致auto推導(dǎo)的類型會與原表達(dá)式的類型存在不一致問題。
為了更好的解決這些問題,從C++11標(biāo)準(zhǔn)開始,C++引入了decltype關(guān)鍵字,其作用是讓編譯器在編譯時(shí)識別表達(dá)式的類型,方便的的進(jìn)行類型推導(dǎo),同時(shí)也解決泛型編程和模板編程中變量類型表示的問題。
標(biāo)準(zhǔn)演進(jìn)
decltype
是declare type的縮寫。C++11標(biāo)準(zhǔn)引入了decltype的核心功能和推導(dǎo)規(guī)則,C++11以后的各標(biāo)準(zhǔn)都本別對C++11自定的規(guī)則進(jìn)行擴(kuò)容和改進(jìn)。具體演進(jìn)過程如下所示:
- C++11:引入關(guān)鍵字,并引入decltype的核心功能,用于根據(jù)表達(dá)式推導(dǎo)出變量的類型;
- C++14:引入兩個(gè)重要改進(jìn)
- 引入decltype(auto)語法,此語法可用于函數(shù)返回值類型的推導(dǎo)?;赿ecltype(auto)語法,函數(shù)的返回值類型可通過函數(shù)體的返回值表達(dá)式來推導(dǎo),從而簡化函數(shù)返回值類型的聲明。
- 放寬了對不完整類型的限制:在 C++11 中,如果 decltype 推導(dǎo)的表達(dá)式結(jié)果是一個(gè)不完整類型,那么會導(dǎo)致編譯錯(cuò)誤。而在 C++14 中,對不完整類型的處理更加寬松,允許使用decltype 推導(dǎo)不完整類型的變量。
- C++17:decltype(atuo)支持非類型模板形參占位符。
C++11
引入關(guān)鍵字,并引入decltype
的核心功能,用于根據(jù)表達(dá)式推導(dǎo)出變量的類型;當(dāng)使用decltype(e)
推導(dǎo)表達(dá)式 e(類型為T)的類型時(shí),C++11標(biāo)準(zhǔn)定義decltype
的推導(dǎo)規(guī)則如下:
- 如果是一個(gè)未加括號的標(biāo)識符表達(dá)式或類成員訪問,那么decltype(e)的推導(dǎo)結(jié)果為e類型T;假如不存在這樣的實(shí)體或e是一組重載函數(shù),那么decltype(e)無法推導(dǎo)。而且推導(dǎo)過程const/volatile 限定符會被忽略;
- 如果e是一個(gè)可調(diào)用對象,那么decltype(e)推導(dǎo)為可調(diào)用對象返回值的類型;
- 如果e是一個(gè)左值,decltype(e)推導(dǎo)為T&。const/volatile 限定符不能忽略;
- 如果e是一個(gè)將亡值,decltype(e)推導(dǎo)為T&&,const/volatile 限定符不可忽略;
- 如decltype(e)無法命中上述4情況,decltype(e)將會推導(dǎo)為e的類型T;
為了讓大家更形象的理解這5條規(guī)則,下面我們通過一些示例來說明這五條推導(dǎo)規(guī)則。
示例 1: 未加括號標(biāo)識符表達(dá)式
int x = 42; decltype(x) y; // 推導(dǎo)結(jié)果是 int,滿足第1條規(guī)則
示例 2: 加括號的標(biāo)識符表達(dá)式
int x = 42; decltype((x)) y = x; // 推導(dǎo)結(jié)果是 int&,滿足第三條規(guī)則
示例3:未加括號的類成員訪問
struct MyClass { int member; }; const MyClass obj; decltype(obj.member) result = obj.member; // 推導(dǎo)結(jié)果是 int, 忽略const/volatile 限定符,滿足第1條規(guī)則
示例4:加括號的類成員訪問
struct MyClass { int member; }; const MyClass obj; decltype((bj.member)) result = obj.member; // 推導(dǎo)結(jié)果是 const int&, const/volatile 限定符不能忽略,滿足第3條規(guī)則
示例 5: 可調(diào)用對象表達(dá)式
int add(int a, int b) { return a + b; } decltype(add(1, 2)) result; // 推導(dǎo)結(jié)果是 int,滿足第2條規(guī)則
示例 6: 將亡值
int x = 42; decltype(std::move(x)) result = std::move(x); // 推導(dǎo)結(jié)果為int&&,std::move(x) 為將亡值
示例 7: 右值表達(dá)式
int x = 42; decltype(x + 1) result; // 推導(dǎo)結(jié)果是 int(右值表達(dá)式 x + 1 的類型是 int)
示例8:右值引用變量
int&& i = 500; decltype(i) x2; // x2的類型是int&&,滿足第5條
C++14
C++14主要引進(jìn)了兩個(gè)重要改進(jìn),他們分別是:放寬對不完整類型的限制;引入decltype(auto)
語法。
放寬對不完整類型的限制
C++11標(biāo)準(zhǔn)要求decltype
在使用時(shí),推導(dǎo)的表達(dá)式必須是完整類型。如果decltype
推導(dǎo)的表達(dá)式是一個(gè)不完整類型,例如某個(gè)類的聲明但尚未定義,那么會導(dǎo)致編譯錯(cuò)誤。C++14對這個(gè)限制進(jìn)行了放寬,允許使用decltype
推導(dǎo)不完整類型的變量。這使得編寫一些特定的模板代碼更加方便,因?yàn)樵谀承┣闆r下,可能需要推導(dǎo)出不完整類型。
但是,雖然C++14放寬了對不完整類型的限制,但仍然要求推導(dǎo)的表達(dá)式在使用時(shí)必須是可見的,即需要在推導(dǎo)時(shí)至少對類型進(jìn)行了前向聲明。否則,將會導(dǎo)致編譯錯(cuò)誤。
以下是一個(gè)示例,演示如何在泛型編程中使用 decltype 推導(dǎo)不完整類型:
template <typename T> struct Container { using ValueType = decltype(*std::declval<T>()); // 使用 decltype 推導(dǎo)不完整類型 // 其他成員和函數(shù)... }; int main() { Container<std::vector<int>> container; using ValueType = typename decltype(container)::ValueType; // 推導(dǎo)結(jié)果為 int& return 0; }
decltype(auto)
除了放寬對不完整類型的限制,C++14還有一個(gè)特色就是decltype(auto)
。decltype(auto)
作用是告訴編譯器auto的推導(dǎo)規(guī)則遵循decltype而非auto。不過有一點(diǎn)需要注意就是decltype(auto)必須單獨(dú)聲明,不能與其他相結(jié)合。所以下述聲明是不合法的:decltype(auto)*
,const decltype(auto)
, volatile decltype(auto)
。
decltype(auto)
的推導(dǎo)規(guī)則如下:
- 如果初始化表達(dá)式是一個(gè)標(biāo)識符表達(dá)式,那么decltype(auto)推導(dǎo)為表達(dá)式的類型(const/volatile 限定符和引用修飾符不能忽略);
- 如果初始化表達(dá)式是一個(gè)函數(shù)調(diào)用表達(dá)式,那么decltype(auto)推導(dǎo)為函數(shù)調(diào)用表達(dá)式的返回類型;
- 如果初始化表達(dá)式是一個(gè)左值表達(dá)式(如變量名、數(shù)組名、成員訪問等),那么decltype(auto)推導(dǎo)為對應(yīng)左值類型的引用類型(const/volatile 限定符和引用修飾符不能忽略)。
- 如果初始化表達(dá)式是一個(gè)右值表達(dá)式(如字面值、臨時(shí)對象、表達(dá)式的結(jié)果等),那么decltype(auto)推導(dǎo)為對應(yīng)右值的類型(const/volatile 限定符和引用修飾符不能忽略)。
- 如果初始化表達(dá)式是一個(gè)將亡值(如移動賦值),那么decltype(auto)推導(dǎo)為對應(yīng)類型的右值引用
示例 1:標(biāo)識符表達(dá)式
int x = 42; decltype(auto) y = x; // 推導(dǎo)結(jié)果是 int(x 的類型)
示例 2:函數(shù)調(diào)用表達(dá)式
int add(int a, int b) { return a + b; } decltype(auto) result = add(1, 2); // 推導(dǎo)結(jié)果是 int(add 函數(shù)返回類型)
示例 3:左值表達(dá)式
const int x = 42; decltype(auto) ref = (x); // 推導(dǎo)結(jié)果是 const int&(x 的引用類型)
示例4:右值表達(dá)式
decltype(auto) x2 = 50; // 推導(dǎo)結(jié)果是 int
示例4:將亡值
int x2 = 50; decltype(auto) x3 = std::move(x2); // 推導(dǎo)結(jié)果為int&&
除了變量類型推導(dǎo)以外,在C++14中引入了decltype(auto)作為一種返回類型的語法。它用于在函數(shù)聲明中指定返回類型,該返回類型將從函數(shù)體中的表達(dá)式推導(dǎo)而來。
為了更好的理解decltype(auto)作為一種返回類型的語法,我們參考下面三種函數(shù)返回類型自動推導(dǎo)定義方式。
第一種: C++14 基于auto新特性返回值類型自動推導(dǎo)
template<typename Container, typename Index> auto accessOrUpdate(Container& c, Index i) { return c[i]; // 返回類推導(dǎo)為c[i]的類型,而且會異常引用限制 } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10; // 編譯錯(cuò)誤,不允許賦值
第二種:C++14 基于auto和decltype實(shí)現(xiàn)返回值類型推導(dǎo)
template <typename Container, typename Index> auto accessOrUpdate(Container &c, Index i) -> decltype(c[i]) { return c[i]; } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10;
第三種:C++14 decltype(auto)實(shí)現(xiàn)返回值類型推導(dǎo)
template <typename Container, typename Index> decltype(auto) accessOrUpdate(Container &c, Index i) { return c[i]; } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10;
對比上述三種函數(shù)返回值類型推導(dǎo),decltype(auto)可讓編譯器根據(jù)表達(dá)式的類型自動推導(dǎo)函數(shù)的返回類型,而不需要顯式地指定返回類型。這種方式可簡化代碼,而且推導(dǎo)更加靈活。
C++17
為與auto交相輝映,C++17開始支持decltype(auto)非類型模板。但是需特別注意的是在C++17標(biāo)準(zhǔn)中,非類型模板參數(shù)類型必須是整理類型(int, short, long等),枚舉類型,指針類型,左值引用類型和std::nullptr_t,而自定義類型,浮點(diǎn)數(shù)和字符串則不允許作非類型模板參數(shù)。
template<decltype(auto) n> // C++17 decltype(auto)形參聲明 auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能從花括號初始化器列表推導(dǎo) { return {n, n}; } f<5>(); // n為int f<(5)>(); // n為int& f<'a'>(); // n為char f<('a')>(); // n為char& f<1.0>(); // 編譯失敗double不能作為模板參數(shù),double不允許做非類型模板參數(shù)。
C++20允許字面量類類型作為非類型模板參數(shù)。例如在C++20之前,下述代碼無法編譯通過,而在C++20中則可以編譯通過。
class A {}; template <A a> class B {}; A a; B<a> b; // C++20 前編譯失敗,C++20 可以編譯成功。
總結(jié)
本文從泛型編程中經(jīng)常會遇到2個(gè)常見問題入手,循序漸進(jìn)的分析了從C++11開始引入的關(guān)鍵字decltype,希望本文可以對大家有所幫助。
相關(guān)文章
C語言實(shí)現(xiàn)最簡單的剪刀石頭布小游戲示例
這篇文章主要介紹了C語言實(shí)現(xiàn)最簡單的剪刀石頭布小游戲,涉及C語言數(shù)組、隨機(jī)數(shù)與數(shù)值運(yùn)算等相關(guān)操作技巧,需要的朋友可以參考下2017-09-09剖析C++中的常量表達(dá)式與省略號的相關(guān)作用
這篇文章主要介紹了C++中的常量表達(dá)式與省略號的相關(guān)作用,以及表達(dá)式中的可變參數(shù)模板示例,需要的朋友可以參考下2016-01-01C++數(shù)據(jù)結(jié)構(gòu)與算法之判斷一個(gè)鏈表是否為回文結(jié)構(gòu)的方法
這篇文章主要介紹了C++數(shù)據(jù)結(jié)構(gòu)與算法之判斷一個(gè)鏈表是否為回文結(jié)構(gòu)的方法,結(jié)合實(shí)例形式分析了回文結(jié)構(gòu)并結(jié)合實(shí)例給出了C++判斷回文的操作技巧,需要的朋友可以參考下2017-05-05深入解析C++11?lambda表達(dá)式/包裝器/線程庫
這篇文章主要介紹了C++11?lambda表達(dá)式/包裝器/線程庫的相關(guān)知識,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05C++實(shí)現(xiàn)LeetCode(133.克隆無向圖)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(133.克隆無向圖),本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07