C++模板類型推導大全

前言和背景

《Effective c++》一書中條款01爲:視c++爲一個語言聯邦,該條款中將c++語言分爲4個次語言組成的“聯邦政府”,其分別爲:兼容基礎c的部分、c++面向對象的部分、c++模板部分、stl庫部分。
我非常認同將c++語言看成由幾個子語言組成的聯邦語言,不過我個人認爲stl庫應該是建立在前三個子語言的基礎上發展出來的,較爲成熟和通用的一個典型作品。(儘管有很多人對stl庫有各種各樣的吐槽,但是話說回來沒被吐槽過的c++庫真是少之又少...),所以我個人把c++語言分爲兼容c部分(隨着c++和c語言各自的發展和演化,目前已經不是一個完全包含的關係了)、c++面向對象部分和c++模板部分,這三個部分各自獨立成一體系,相互之間又有很深刻的聯繫。有很多人說c++模板就是c++標準提供給程序員的,用來生成自定義的函數和類的腳本語言,只不過這種腳本語言被c++納入標準了而已。
C++模板部分的基石,我個人認爲是模板類型推導和模板的特化與偏特化。從基礎的stl庫的各種組件的使用到自己進行模板元編程,都是在這兩個基本概念之上發展起來的。這篇文章的目的就是記錄c++11/14標準下的c++模板類型推導規則。

C++的聲明語法:
decl-specifier-seq init-declarator-list

decl-specifier-seq可以是以下幾種(順序任意):

  • 1.typedef 說明符,如果出現的話說明整個聲明是一個typedef聲明,該聲明會introduce一個新的類型名稱,而不是一個函數或者對象。
  • 2.inline說明符,(c++17開始允許出現在變量聲明中)
  • 3.friend說明符,允許出現在類和函數聲明中。
  • 4.constexpr說明符(c++11),允許出現在變量定義、函數和函數模板聲明以及靜態數據成員的聲明中(必須用字面值初始化)。
  • 5.存儲週期說明符(register(until c++11),static,thread_local,extern,mutable)
  • 6.type specifiers,其中包括:
  • (1)class declaration
  • (2)enum declaration
  • (3)內置類型說明符
  • (4)auto、decltype specifier(根據表達式來推導類型的兩個說明符)
  • (5)之前聲明的類和enum名字
  • (6)之前聲明的typedef-name或者type alias
  • (7)模板參數填充的模板名字
  • (8)elaborated type 說明符,沒用過
  • (9)typename specifier
  • (10)cv說明符(const volatile)
  • (11)attributes這裏不介紹,沒用過

注:上述出現的specifier並不僅僅翻譯爲說明符,它與前面的關鍵字作爲一個整體,代表了某一段語法規則而不僅僅是其前面的關鍵字而已,具體請查詢:
https://en.cppreference.com/w...
僅只有一個type specifier允許出現在decl-specifier-seq中,但以下情況除外:
1.const和volatile能夠和除了自己以外的其他type specifiers一同出現。
2.signed和unsigned能夠和char、long、short或者int一起出現。
3.short和long能夠和int一起出現。
4.long可以和long一起出現。(c++11)

接下來看init-declarator-list:
init-declarator-list的語法規則我們略過,只說明會有哪些情況:

  • 1.unqualified-id
  • 2.qualified-id
  • 3....
  • 4.指針聲明
  • 5.指向成員的指針聲明
  • 6.左值引用
  • 7.右值引用
  • 8.數組聲明符
  • 9.函數聲明符

儘管上述列出的標準是如此繁雜和無從下手,但我不得不說這已經是一份簡化版本,我略去了c++11之後的標準中提出的內容,以及自己沒怎麼使用過的部分。我列出這些的目的在於理解,c++中的類型由:decl-specifier-seq 和init-declarator-list兩部分共同組成。並且,decl-specifier-seq的部分中,type specifiers參與了類型的真正構成(其餘類型的標識符都說明了聲明的name其他方面的性質)
我們將關注點放在變量的聲明上,以此排除掉類、函數和模板聲明相關的標識符。一個變量的類型如下:

  • 1.可選的cv標識符
  • 2.type specifiers中的標識符
  • 3.可選的*、&、&&、[(可選的constexpr)]標識符

例如:int、const int、const int&、int*、const int[12]等等,都是符合上述條件的類型。我們之後的模板類型推導也正是建立在這個前提下。

類型推導

此節關於模板類型推導、auto類型推導和decltype。大部分借鑑於《Effective modern c++》
首先來看模板類型推導

template<typename T>
void f(ParamType param);
...
f(expr);

如上,模板類型T的類型由ParamType和expr聯合決定,分如下幾種情況:

  • ParamType是引用或者指針,但不是萬能引用:

如果expr的類型是引用類型,丟掉其引用類型。然後用expr的類型去匹配ParamType以確定T的真實類型。我們在前言中提到過,任何一個變量的類型由三部分組成:可選的cv標識符、裸類型(我自己這麼稱呼,代表不具有cv屬性、不具有引用、指針標識符後綴的類型,數組和函數標識符標識的類型在模板類型推導中是特殊情況,我們在最後討論)和可選的引用、指針、數組、函數標識符。這裏指的是,先用expr中的裸類型確定T的裸類型(直接匹配),然後如果expr或者ParamType中的任何一個具有cv標識符,則給T的裸類型前加cv標識符,推導完成。

template<class T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);             // T是int,ParamType是int&
f(cx);             //T是const int,ParamType是const int&
f(rx);            //T是const int,ParamType是const int&

將f中的T& 換位const T& ,結果不變。
將上述兩種情況中的&換爲*,結果不變。

  • ParamType是萬能引用類型:

萬能引用類型的語法格式很固定:

template<class T>
void f(T&& param);

這裏面的ParamType是萬能引用類型。注:關於c++中的萬能引用類型,effective modern c++一書中有章節專門講這個:Item24 。
這種情況對於模板類型T的推導和情況1完全不同,因爲這裏涉及到了c++中表達式結果的一個性質:value category。說到這個又是一個十分基礎和重要的性質,c++中的表達式的結果有兩個獨立的性質:type和value category。其中type很好理解,例如int i = 2; (i)這個表達式的type就是int&,就是我們理解的變量的類型。而value category有童鞋聽過的話就是我們說的左值、純右值、消亡值等等概念,這個性質代表了變量的生命週期、能否對其進行取地址操作等。理解value category的概念對於c++11非常重要,右值引用、完美轉發、std::move、移動構造函數和移動賦值函數等都建立在以下幾個基本概念上:value category、模板類型推導、引用摺疊規則和右值引用。大家可以打開編譯器看看move的一份實現,短短几行代碼幾乎凝聚了c++11帶來的大部分根本性的擴展:

        // FUNCTION TEMPLATE move
template<class _Ty>
    _NODISCARD constexpr remove_reference_t<_Ty>&&
        move(_Ty&& _Arg) _NOEXCEPT
    {    // forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
    }

羅裏吧嗦說了這麼多,好像還沒開始我們的正題。但是c++就是這麼一門語言,理解c++中構成其大部分組件的基石是進階必要的。

如果expr是一個lvalue,T會被推斷爲對應expr裸類型的左值引用,而根據引用摺疊規則,ParamType也被推斷爲左值引用。
如果expr是一個rvalue(純右值+消亡值統稱爲右值),情況1生效,即expr會被推斷爲對應的裸類型,而ParamType將會是裸類型對應的右值引用類型。
這裏我舉幾個十分經典的例子:
例子1:

1.template<class T>
void f(const T&& param); //這不是萬能引用,見effective modern c++ item24
...
const int i = 2;
f(i);

會發生什麼?模板類型推導成功,但程序編譯不過,因爲不是萬能引用,用情況1的條件去推導,得出T的類型是int,那麼該模板函數會生成如下函數:
void f(const int&& param);
f(i)
相當於用常量右值引用去綁定i,而i是一個lvalue,所以編譯會不通過。

例子2:

template<class T>
void f(T&& param);
...
int i = 2;
f(i);              //T是int&
f(std::move(i));    //T是 int

兩次調用都觸發了第二種萬能引用情況,但是i是一個lvalue,而std::move(i)返回的是一個type爲int&&,value category爲xvalue的變量,該變量是一個rvalue,所以這兩種調用分別對應了2中的兩種情況。
注:std::move之所以會返回xvalue變量,因爲c++標準規定static_cast類型轉換表達式當把某個變量的類型轉化爲對應的右值引用類型時,會將其value category變爲xvalue.見:
https://en.cppreference.com/w...

  • ParamType既不是指針也不是引用
template<class T>
void f(T param);

或者

template<class T>
void f2(const T param);

如果expr的類型中含有引用,則忽略其引用。如果expr的類型中含有cv,也將其忽略,然後用expr的裸類型去匹配T。
注意到,這和情況1比較像,區別在於模板類型T不會繼承expr的cv屬性,理由如下:由於ParamType既不是指針也不是引用,故此函數調用將會是值傳遞,實參是形參copy過來的,而形參的cv屬性是用來限制形參本身,現在實參已經是一份複製品,對其修改也不會影響形參。

三種情況介紹完了,之後需要補充c++模板類型推導對於數組類型和函數類型的特殊處理。
對於數組類型,如果ParamType符合情況1和2,則T會被推導爲對應裸類型的數組類型的應用類型,例如:

template<class T>
void func(T&& param); //or void func(T& param);
...
int i[2] = {1,1};
func(i);                    //T會被推導爲int(&)[2]類型。

如果ParamType符合情況3,則T會被推導爲對應裸類型的指針類型。

對於函數類型,由於c++中函數類型會隱式轉化爲函數指針類型,故和數組類型的處理方法是一樣的(數組類型也是c++中隱式轉化爲指針類型的)。

這就是關於模板類型推導的內容,三種主流情況與兩個特例。本節涉及到的基本概念有:
1.value category
2.右值引用、左值引用、常量左值引用的匹配優先級
3.引用摺疊
4.std::move和完美轉發
5.模板類型推導規則
這些概念十分重要,c++中很多高級特性都是在這些基本概念上搭建起來的,基本概念5和模板特化與偏特化,這兩塊搭建起了c++模板的絕大部分內容;基本概念2+3+4是理解c++移動構造函數、移動賦值函數的基石。

文章寫的倉促,不當之處頗多,並且關於auto類型推導和decltype的使用還沒有介紹,之後再仔細校對填充。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章