C++ decltype(型別推導)精講

2020-07-16 10:05:18
學習了《C++ auto》一節我們應該知道,auto 用於通過一個表示式在編譯時確定待定義的變數型別,auto 所修飾的變數必須被初始化,編譯器需要通過初始化來確定 auto 所代表的型別,即必須要定義變數。若僅希望得到型別,而不需要(或不能)定義變數的時候應該怎麼辦呢?

C++11 新增了 decltype 關鍵字,用來在編譯時推匯出一個表示式的型別。它的語法格式如下:

decltype(exp)

其中,exp 表示一個表示式(expression)。

從格式上來看,decltype 很像 sizeof ——用來推導表示式型別大小的操作符。類似於 sizeof,decltype 的推導過程是在編譯期完成的,並且不會真正計算表示式的值。

那麼怎樣使用 decltype 來得到表示式的型別呢?讓我們來看一組例子:
int x = 0;
decltype(x) y = 1;           // y -> int
decltype(x + y) z = 0;       // z -> int
const int& i = x;
decltype(i) j = y;           // j -> const int &
const decltype(z) * p = &z;  // *p  -> const int, p  -> const int *
decltype(z) * pi = &z;       // *pi -> int      , pi -> int *
decltype(pi)* pp = π      // *pp -> int *    , pp -> int * *
對程式碼的說明:
1) y 和 z 的結果表明 decltype 可以根據表示式直接推匯出它的型別本身。這個功能和上一節的 auto 很像,但又有所不同。auto 只能根據變數的初始化表示式推匯出變數應該具有的型別。若想要通過某個表示式得到型別,但不希望新變數和這個表示式具有同樣的值,此時 auto 就顯得不適用了。

2) j 的結果表明 decltype 通過表示式得到的型別,可以保留住表示式的參照及 const 限定符。實際上,對於一般的標記符表示式(id-expression),decltype 將精確地推匯出表示式定義本身的型別,不會像 auto 那樣在某些情況下捨棄掉參照和 cv 限定符。

3) p、pi 的結果表明 decltype 可以像 auto 一樣,加上參照和指標,以及 cv 限定符。

4) pp 的推導則表明,當表示式是一個指標的時候,decltype 仍然推匯出表示式的實際型別(指標型別),之後結合 pp 定義時的指標標記,得到的 pp 是一個二維指標型別。這也是和 auto 推導不同的一點。

對於 decltype 和參照(&)結合的推導結果,與 C++11 中新增的參照折疊規則(Reference Collapsing)有關,因此,留到後面右值參照(Rvalue Reference)一節再詳細講解。

擴充套件閱讀

關於 p、pi、pp 的推導,有個很有意思的地方。像 Microsoft Visual Studio 這樣的 IDE,可以在執行時觀察每個變數的型別。我們可以看到 p 的顯示是這樣的:

這其實是 C/C++ 的一個違反常理的地方:指標(*)、參照(&)屬於說明符(declarators),在定義的時候,是和變數名,而不是型別識別符號(type-specifiers)相結合的。

因此,const decltype(z)*p推匯出來的其實是 *p 的型別(const int),然後再進一步運算出 p 的型別。

decltype 的推導規則

從前面的內容來看,decltype 的使用是比較簡單的。但在簡單的使用方法之後,也隱藏了不少細節。

我們先來看看 decltype(exp) 的推導規則:
  • 推導規則1,exp 是識別符號、類存取表示式,decltype(exp) 和 exp 的型別一致。
  • 推導規則2,exp 是函數呼叫,decltype(exp) 和返回值的型別一致。
  • 推導規則3,其他情況,若 exp 是一個左值,則 decltype(exp) 是 exp 型別的左值參照,否則和 exp 型別一致。
關於推導規則,有很多種版本:
C++ 標準:ISO/IEC 14882:2011,7.1.6.2 Simple type specif iers,第4 款
MSDN:decltype Type Specif ier,http://msdn.microsoft.com/en-us/library/dd537655.aspx
維基百科:decltype,http://en.wikipedia.org/wiki/Decltype

雖然描述不同,但其實是等價的。為了方便理解,這裡選取了 MSDN 的版本。
只看上面的推導規則,很難理解decltype(exp) 到底是一個什麼型別。為了更好地講解這些規則的適用場景,下面根據上面的規則分3種情況依次討論:
  • 識別符號表示式和類存取表示式。
  • 函數呼叫(非識別符號表示式,也非類存取表示式)。
  • 帶括號的表示式和加法運算表示式(其他情況)。

1) 識別符號表示式和類存取表示式

先看第一種情況,下面的程式碼是一組簡單的例子。

【範例】decltype 作用於識別符號和類存取表示式範例。
class Foo
{
    public:
    static const int Number = 0;
    int x;
};
int n = 0;
volatile const int & x = n;
decltype(n) a = n;            // a -> int
decltype(x) b = n;            // b -> const volatile int &
decltype(Foo::Number) c = 0;  // c -> const int
Foo foo;
decltype(foo.x) d = 0;        // d -> int,類存取表示式
變數 a、b、c 保留了表示式的所有屬性(cv、參照)。這裡的結果是很簡單的,按照推導規則1,對於識別符號表示式而言,decltype 的推導結果就和這個變數的型別定義一致。

d 是一個類存取表示式,因此也符合推導規則1。

2) 函數呼叫

接下來,考慮第二種情況:如果表示式是一個函數呼叫(不符合推導規則1),結果會如何呢?請看下面的程式碼。

【範例】decltype 作用於函數呼叫的範例。
int& func_int_r(void);           // 左值(lvalue,可簡單理解為可定址值)
int&& func_int_rr(void);         // x值(xvalue,右值參照本身是一個xvalue)
int func_int(void);              // 純右值(prvalue,將在後面的章節中講解)
const int& func_cint_r(void);    // 左值
const int&& func_cint_rr(void);  // x值
const int func_cint(void);       // 純右值
const Foo func_cfoo(void);       // 純右值
// 下面是測試語句
int x = 0;
decltype(func_int_r())   a1 = x;      // a1 -> int &
decltype(func_int_rr())  b1 = 0;      // b1 -> int &&
decltype(func_int())     c1 = 0;      // c1 -> int
decltype(func_cint_r())  a2 = x;      // a2 -> const int &
decltype(func_cint_rr()) b2 = 0;      // b2 -> const int &&
decltype(func_cint())    c2 = 0;      // c2 -> int
decltype(func_cfoo())    ff = Foo();  // ff -> const Foo
可以看到,按照推導規則2,decltype 的結果和函數的返回值型別保持一致。

這裡需要注意的是,c2 是 int 而不是 const int。這是因為函數返回的 int 是一個純右值(prvalue)。對於純右值而言,只有類型別可以攜帶 cv 限定符,此外則一般忽略掉 cv 限定。

如果在 gcc 下編譯上面的程式碼,會得到一個警告資訊如下:

warning: type qualif?iers ignored on function return type [-Wignored-qualifiers] cint func_cint(void);

因此,decltype 推匯出來的 c2 是一個 int。

作為對比,可以看到 decltype 根據 func_cfoo() 推匯出來的 ff 的型別是 const Foo。

3) 帶括號的表示式和加法運算表示式

最後,來看看第三種情況:
struct Foo { int x; };
const Foo foo = Foo();
decltype(foo.x)   a = 0;  // a -> int
decltype((foo.x)) b = a;  // b -> const int &
int n = 0, m = 0;
decltype(n + m) c = 0;    // c -> int
decltype(n += m) d = c;   // d -> int &
a 和 b 的結果:僅僅多加了一對括號,它們得到的型別卻是不同的。

a 的結果是很直接的,根據推導規則1,a 的型別就是 foo.x 的定義型別。

b 的結果並不適用於推導規則1和2。根據 foo.x 是一個左值,可知括號表示式也是一個左值。因此可以按照推導規則3,知道 decltype 的結果將是一個左值參照。

foo 的定義是 const Foo,所以 foo.x 是一個 const int 型別左值,因此 decltype 的推導結果是 const int&。

同樣,n+m 返回一個右值,按照推導規則3,decltype 的結果為 int。

最後,n+=m 返回一個左值,按照推導規則3,decltype 的結果為 int&。

decltype 的實際應用

decltype 的應用多出現在泛型程式設計中,請看下面的例子。

【範例】泛型型別定義可能存在問題的範例。
#include <vector>
template <class ContainerT>
class Foo
{
    typename ContainerT::iterator it_; // 型別定義可能有問題
public:
    void func(ContainerT& container)
    {
        it_ = container.begin();
    }
    // ...
};
int main(void)
{
    typedef const std::vector<int> container_t;
    container_t arr;
    Foo<container_t> foo;
    foo.func(arr);
    return 0;
}
單獨看類 Foo 中的 it_ 成員定義,很難看出會有什麼錯誤,但在使用時,若上下文要求傳入一個 const 容器型別,編譯器馬上會彈出一大堆錯誤資訊。

原因就在於,ContainerT::iterator 並不能包括所有的疊代器型別,當 ContainerT 是一個 const 型別時,應當使用 const_iterator。

要想解決這個問題,在 C++98/03 下只能想辦法把 const 型別的容器用模板特化單獨處理,比如增加一個像下面這樣的模板特化:
template <class ContainerT>
class Foo<const ContainerT>
{
    typename ContainerT::const_iterator it_;
public:
    void func(const ContainerT& container)
    {
        it_ = container.begin();
    }
    // ...
};
這實在不能說是一個好的解決辦法。若 const 型別的特化只是為了配合疊代器的型別限制,Foo 的其他程式碼也不得不重新寫一次。

有了 decltype 以後,就可以直接這樣寫:
template <class ContainerT>
class Foo
{
    decltype(ContainerT().begin()) it_;
public:
    void func(ContainerT& container)
    {
        it_ = container.begin();
    }
    // ...
};
是不是舒服很多了?

decltype 也經常用在通過變數表示式抽取變數型別上,如下面的這種用法:
vector<int> v;
// ...
decltype(v)::value_type i = 0;
在冗長的程式碼中,人們往往只會關心變數本身,而並不關心它的具體型別。比如在上例中,只要知道v是一個容器就夠了(可以提取 value_type),後面的所有演算法內容只需要出現 v,而不需要出現像vector<int> 這種精確的型別名稱。這對理解一些變數型別複雜但操作統一的程式碼片段有很大好處。

實際上,標準庫中有些型別都是通過 decltype 來定義的:
typedef decltype(nullptr) nullptr_t;  //通過編譯器關鍵字nullptr定義型別nullptr_t
typedef decltype(sizeof(0)) size_t;
這種定義方法的好處是,從型別的定義過程上就可以看出來這個型別的含義。