Item 3:理解 decltype (Understand decltype)
Overview Table
| 主題 | 重點 |
|---|---|
| decltype 基本行為 | 對名稱或運算式如實回報其型別,不做任何修飾(與 template/auto 推導會剝 reference、const 不同) |
| 名稱 vs 運算式 | 對「未加括號的名稱」回報宣告型別;對「比名稱更複雜的 lvalue 運算式」(型別 T)一律回報 T& |
| 括號陷阱 | decltype(x) 是 int;decltype((x)) 是 int& —— 加括號就從名稱變成 lvalue 運算式 |
| decltype(auto)(C++14) | 像 auto 一樣由 initializer 推導型別,但改用 decltype 規則(保留 reference/const) |
| 典型用途 | 回傳型別依參數而定的 function template(如轉發 operator[] 的回傳型別) |
| auto 回傳的缺陷 | auto 回傳型別用 template 推導規則 → 剝掉 reference,decltype(auto) 才能原樣保留 |
decltype 的基本行為:如實回報型別
與 02-Deducing-Types/01-Template-Type-Deduction、02-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),i 為 const 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;,cw 為 const Widget& |
const Widget&(auto 則推導為 Widget) |
| 「同時接受 lvalue/rvalue 容器」 | 參數改 Container&&(universal reference)並以 std::forward<Container>(c) 使用 |
decltype(v[0]),v 為 vector<int> |
int&;但 vector<bool> 的 [] 回傳 proxy object,非 bool& |
Related Notes
- 02-Deducing-Types/01-Template-Type-Deduction ——
auto回傳型別採用的正是 template 推導規則(reference 會被剝掉),是理解decltype(auto)存在理由的前提 - 02-Deducing-Types/02-Auto-Type-Deduction ——
auto與decltype(auto)對同一個 initializer 可能推導出不同型別 - 02-Deducing-Types/04-Viewing-Deduced-Types —— 用 IDE、編譯錯誤、Boost.TypeIndex 驗證 decltype 推導結果
- 03-Auto/02-Explicitly-Typed-Initializer-Idiom ——
std::vector<bool>::operator[]回傳 proxy type 的完整故事 - 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References ——
Container&& c為何能同時綁定 lvalue 與 rvalue - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage —— 對 universal reference 使用
std::forward的準則 - 07-Lambda-Expressions/03-Decltype-Auto-Forwarding —— 在 generic lambda 中用 decltype 配合
std::forward