Item 3:理解 decltype (Understand decltype)

Overview Table

主題 重點
decltype 基本行為 對名稱或運算式如實回報其型別,不做任何修飾(與 template/auto 推導會剝 reference、const 不同)
名稱 vs 運算式 對「未加括號的名稱」回報宣告型別;對「比名稱更複雜的 lvalue 運算式」(型別 T)一律回報 T&
括號陷阱 decltype(x)intdecltype((x))int& —— 加括號就從名稱變成 lvalue 運算式
decltype(auto)(C++14) auto 一樣由 initializer 推導型別,但改用 decltype 規則(保留 reference/const)
典型用途 回傳型別依參數而定的 function template(如轉發 operator[] 的回傳型別)
auto 回傳的缺陷 auto 回傳型別用 template 推導規則 → 剝掉 referencedecltype(auto) 才能原樣保留

decltype 的基本行為:如實回報型別

02-Deducing-Types/01-Template-Type-Deduction02-Deducing-Types/02-Auto-Type-Deduction 會忽略 reference-ness 或 constness 不同,decltype 幾乎總是原封不動回報你給它的名稱或運算式的型別(Meyers 稱之為 parrot back):

const int i = 0;               // decltype(i) 是 const int(const 保留!)

bool f(const Widget& w);       // decltype(w)    是 const Widget&
                               // decltype(f)    是 bool(const Widget&)
                               // decltype(f(w)) 是 bool(呼叫結果的型別)

struct Point {
  int x, y;                    // decltypex 是 int
};

std::vector<int> v;            // decltype(v) 是 std::vector<int>
// decltype(v[0]) 是 int& —— vector<T>::operator[] 回傳 T&
「幾乎總是」的例外

std::vector<bool>operator[] 回傳 bool&,而是回傳 proxy object(見 03-Auto/02-Explicitly-Typed-Initializer-Idiom)。另外「比名稱更複雜的 lvalue 運算式」會被回報為 T&(見下方括號陷阱)。

主要用途:回傳型別依參數而定(authAndAccess 演進史)

需求:寫一個 template,先驗證使用者身分再回傳 c[i],且回傳型別必須與容器的 operator[] 完全一致。

// C++11:trailing return type(尾置回傳型別)
// 前面的 auto 與型別推導無關,只代表「回傳型別寫在 -> 之後」
// 好處:-> 之後可以使用參數 c 與 i
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
  -> decltype(c[i])                 // 回傳型別 = c[i] 的型別
{
  authenticateUser();
  return c[i];
}

C++14 允許省略 trailing return type、只留 auto —— 但此時 auto 採用 template 型別推導,會剝掉 reference

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)   // C++14 但「不太正確」
{
  authenticateUser();
  return c[i];                     // c[i] 是 int& → 推導成 int(reference 被剝掉)
}

std::deque<int> d;
authAndAccess(d, 5) = 10;          // 編譯失敗!回傳的是 rvalue int,不能被賦值

解法:decltype(auto) —— auto 表示「型別要推導」,decltype 表示「用 decltype 規則推導」:

// 最終 C++14 版本:universal reference + std::forward,lvalue/rvalue 容器都能接
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container&& c, Index i)       // c 是 universal reference
{
  authenticateUser();
  return std::forward<Container>(c)[i];     // 依 Item 25 對 universal ref 用 forward
}

// 對應的 C++11 最終版:得自己寫出回傳型別
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
  -> decltypeforward<Container>(c)[i]
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

decltype(auto) 也可用於變數宣告,讓 initializer 用 decltype 規則推導:

Widget w;
const Widget& cw = w;
auto           myWidget1 = cw;   // auto 推導:Widget(剝掉 const 與 &)
decltype(auto) myWidget2 = cw;   // decltype 推導:const Widget&(原樣保留)

括號陷阱:(x) 是 lvalue 運算式,不是名稱

decltype 對名稱回報宣告型別;但對「非名稱的 lvalue 運算式」(型別 T)一律回報 T&。把名稱 x 包進括號 (x),就變成了「比名稱更複雜的 lvalue 運算式」:

decltype(e) 判定流程
┌────────────────────────────────┐
│ e 是未加括號的名稱?            │──是──> 回報該名稱的「宣告型別」(原樣)
└────────────────────────────────┘
              │否
              v
┌────────────────────────────────┐
│ e 是 lvalue 運算式(型別 T)?  │──是──> 回報 T&
└────────────────────────────────┘         (例:(x)、*p、函式回傳 lvalue)
              │否
              v
       回報 T(prvalue;xvalue 則為 T&&)

搭配 decltype(auto) 時,return 敘述句一對括號就足以改變回傳型別、甚至引發 undefined behavior:

decltype(auto) f1()
{
  int x = 0;
  return x;        // decltype(x) 是 int → f1 回傳 int
}

decltype(auto) f2()
{
  int x = 0;
  return (x);      // decltype((x)) 是 int& → f2 回傳 int&
}                  // 回傳指向區域變數的 reference —— undefined behavior!
decltype(auto) 要極度小心

return 運算式中看似無關緊要的細節(如括號)都會改變推導結果。不確定時,用 02-Deducing-Types/04-Viewing-Deduced-Types 的技巧確認實際推導出的型別。

Exam/Test Patterns

情境關鍵字 答案
decltype(i)iconst int const int —— decltype 保留 const,不像 by-value 推導
int x = 0; decltype(x) int(名稱 → 宣告型別)
int x = 0; decltype((x)) int&(加括號 → lvalue 運算式 → T&)
decltype(auto) 函式 return (x); 回傳 int&,指向區域變數 → undefined behavior
C++14 auto 回傳型別 + return c[i]; reference 被剝掉,回傳 rvalue;authAndAccess(d, 5) = 10 編譯失敗
「回傳型別與 c[i] 完全相同」 C++14 用 decltype(auto);C++11 用 auto ... -> decltypeforward<Container>(c)[i]
decltype(auto) v = cw;cwconst Widget& const Widget&(auto 則推導為 Widget
「同時接受 lvalue/rvalue 容器」 參數改 Container&&(universal reference)並以 std::forward<Container>(c) 使用
decltype(v[0])vvector<int> int&;但 vector<bool>[] 回傳 proxy object,非 bool&