型別推導練習題 (Practice - Deducing Types)
Related Concepts
- 02-Deducing-Types/01-Template-Type-Deduction
- 02-Deducing-Types/02-Auto-Type-Deduction
- 02-Deducing-Types/03-Decltype
- 02-Deducing-Types/04-Viewing-Deduced-Types
| 關鍵字 | 答案 |
|---|---|
f(T& param) 傳入 const 物件 |
reference 剝除、const 保留(const 進入 T) |
f(T param)(by-value) |
reference 與頂層 const/volatile 全部剝除 |
f(T&& param) 傳入 lvalue |
T 與 param 都是 lvalue reference(唯一區分 lvalue/rvalue 的 Case) |
| array / function 名稱 by-value 傳入 | decay 成指標;by-reference 才保留真正型別(含大小) |
auto x = { ... }; |
std::initializer_list<T>(auto 與 template 推導的唯一差異) |
| auto 回傳型別 / lambda 的 auto 參數 | 用 template 推導,不接受 braced initializer |
decltype(名稱) |
如實回報宣告型別(const、& 都保留) |
decltype((x))(加括號) |
lvalue 運算式 → 回報 T& |
decltype(auto) |
用 decltype 規則推導,保留 reference/const |
typeid(...).name() |
不可靠:規格強制套用 by-value 規則(去 &、去頂層 cv) |
| 準確檢視型別 | 編譯期用未定義模板 TD;執行期用 Boost.TypeIndex |
Question 1 - Case 1 參考參數推導 [recall]
模板宣告為
template<typename T> void f(T& param);,依序傳入int x、const int cx、const int& rx,各呼叫的 T 與 param 型別為何?
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T = ? param = ?
f(cx); // T = ? param = ?
f(rx); // T = ? param = ?
f(x):T = int,param = int&;f(cx):T = const int,param = const int&;f(rx):T = const int,param = const int&。
Case 1 規則:先忽略 expr 的 reference-ness(rx 的 & 被剝除),但 constness 保留並成為 T 的一部分——這保證傳 const 物件給 T& 參數是安全的。
Question 2 - By-Value 只剝頂層 const [recall]
將
const char* const ptr = "Fun with pointers";傳入template<typename T> void f(T param);,T 推導為什麼型別?
T = const char*。by-value 推導只剝除 expr 本身的 constness(頂層 const,即指標自身不可改指向),因為 param 是全新副本;但指標所指內容的 const 保留——副本可以改指向別處,仍不能修改字串內容。
Question 3 - Array 引數的 Decay [recall]
const char name[] = "J. P. Briggs";(型別const char[13])分別傳入 by-value 模板f(T param)與 by-reference 模板f(T& param),T 各是什麼?
by-value:T = const char*(array decay 成指標,因為函式參數沒有真正的 array 型別)。
by-reference:T = const char [13],param = const char (&)[13]——保留含大小的真正 array 型別。function 名稱同理:by-value 得函式指標 void (*)(int, double),by-reference 得函式參考 void (&)(int, double)。
Question 4 - Universal Reference 推導 [application]
模板為
template<typename T> void f(T&& param);,請寫出以下四個呼叫各自推導出的 T 與 param 型別。
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T = ? param = ?
f(cx); // T = ? param = ?
f(rx); // T = ? param = ?
f(27); // T = ? param = ?
f(x):T = int&,param = int&;f(cx):T = const int&,param = const int&;f(rx):T = const int&,param = const int&;f(27):27 是 rvalue → 套 Case 1,T = int,param = int&&。
Universal reference 是唯一依 lvalue/rvalue 區分引數的推導情境,也是 T 唯一會被推導為 reference 的情況——這是 perfect forwarding 的基礎(詳見 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References)。
Question 5 - auto 與 Braced Initializer [recall]
auto x1 = 27;、auto x2(27);、auto x3 = { 27 };、auto x4{ 27 };四個宣告(依原書 C++11/14 規則)各推導出什麼型別?
x1、x2 是 int;x3、x4 是 std::initializer_list<int>(含單一元素 27)。
這是 auto 推導與 template 推導的唯一差異:auto 假設 braced initializer 代表 std::initializer_list。
注意版本差異:C++17(N3922)起 direct-list-initialization auto x4{ 27 }; 改推導為 int,只有 auto x3 = { 27 }; 仍是 std::initializer_list<int>。
Question 6 - auto 回傳型別遇上大括號 [recall]
函式
auto createInitList() { return { 1, 2, 3 }; }能否編譯?為什麼?
不能編譯。函式回傳型別的 auto(以及 C++14 generic lambda 的 auto 參數)採用的是 template type deduction 而非 auto type deduction,而 template 推導不假設 { 1, 2, 3 } 是 std::initializer_list,直接推導失敗。
Question 7 - auto&& 宣告組 [application]
已知
int x = 27; const int cx = x;,請推導auto&& uref1 = x;、auto&& uref2 = cx;、auto&& uref3 = 27;三者的型別。
uref1 = int&(x 是 int lvalue);uref2 = const int&(cx 是 const int lvalue);uref3 = int&&(27 是 rvalue)。
auto&& 就是 universal reference:auto 扮演 T、type specifier 扮演 ParamType,完全對應 Item 1 的 Case 2 規則。
Question 8 - decltype 的括號陷阱 [recall]
int x = 0;時,decltype(x)與decltype((x))分別是什麼型別?
decltype(x) = int(未加括號的名稱 → 如實回報宣告型別)。
decltype((x)) = int&——(x) 是「比名稱更複雜的 lvalue 運算式」,decltype 對型別為 T 的非名稱 lvalue 運算式一律回報 T&。
Question 9 - decltype(auto) 變數宣告 [application]
Widget w; const Widget& cw = w;之後,auto myWidget1 = cw;與decltype(auto) myWidget2 = cw;各推導出什麼型別?
myWidget1 = Widget:auto(by-value 推導)剝除 reference 與 const。
myWidget2 = const Widget&:decltype(auto) 中 auto 表示「型別要推導」、decltype 表示「用 decltype 規則推導」——對名稱 cw 如實回報宣告型別,reference 與 const 原樣保留。
Question 10 - authAndAccess 的回傳型別 [analysis]
以下 C++14 模板意圖回傳
c[i]供呼叫端賦值,但authAndAccess(d, 5) = 10;卻編譯失敗——請分析原因並給出最終正確版本。
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) // 有問題的版本
{
authenticateUser();
return c[i];
}
std::deque<int> d;
authAndAccess(d, 5) = 10; // 編譯失敗!
原因:回傳型別的 auto 用 template 推導,d[5] 回傳的 int& 被剝除 reference 而成 int;函式回傳的 int 是 rvalue,對 rvalue 賦值被 C++ 禁止。
修正一:回傳型別改用 decltype(auto),以 decltype 規則保留 c[i] 的 reference。
修正二:參數改 Container&& c(universal reference)以同時接受 lvalue/rvalue 容器,並依 Item 25 以 return std::forward<Container>(c)[i]; 轉發。C++11 版則需自寫 trailing return type:auto ... -> decltypeforward<Container>(c)[i]。
Question 11 - typeid 為何不可靠 [recall]
在
template<typename T> void f(const T& param)內用typeid(param).name()印型別,param 真正型別為const Widget* const&,卻印出const Widget*——為什麼?
規格強制 std::type_info::name 把型別視為「以 by-value 傳入模板函式」處理:先去除 reference-ness,再去除頂層 const/volatile(正是 Item 1 的 Case 3 規則),因此對 reference 或 cv 修飾型別的回報必然失真,不是實作品質問題。
執行期要準確結果需改用 Boost.TypeIndex 的 type_id_with_cvr<T>().pretty_name()(不移除 cv 與 reference 修飾)。
Question 12 - 編譯期檢視型別的技巧 [recall]
不依賴任何函式庫,如何讓編譯器準確「說出」
auto x = theAnswer;推導的型別?
宣告一個只有宣告、沒有定義的類別模板 template<typename T> class TD;(Type Displayer),再寫 TD<decltype(x)> xType; 觸發實體化失敗——錯誤訊息必然包含完整型別,如 error: aggregate 'TD<int> xType' has incomplete type。
變數命名成 xType 這種 variableNameType 形式,可讓錯誤訊息更容易搜尋定位。
Question 13 - return x 與 return (x) 的天壤之別 [analysis]
兩個
decltype(auto)函式,一個return x;、一個return (x);(x 為區域變數int x = 0;)——請分析兩者回傳型別的差異與後者的嚴重後果。
decltype(auto) f1() { int x = 0; return x; }
decltype(auto) f2() { int x = 0; return (x); }
f1:decltype(x) 是 int(名稱 → 宣告型別)→ 回傳 int,安全。
f2:(x) 是 lvalue 運算式,decltype((x)) 是 int& → 回傳指向已銷毀區域變數的 reference,是 undefined behavior。
教訓:decltype(auto) 對 return 運算式的細節極度敏感,一對看似無意義的括號就能改變回傳型別並引入 UB;不確定時用 Item 4 的工具(TD 模板、Boost.TypeIndex)驗證實際推導結果。
| 模式 | 一句話結論 |
|---|---|
Template 推導 Case 1(T&/T*) |
剝 reference、保留 const |
Template 推導 Case 2(T&&) |
lvalue → T 為 lvalue reference;rvalue → 套 Case 1 |
| Template 推導 Case 3(by-value) | 剝 reference 與頂層 cv;底層 const 保留 |
| Array / Function 引數 | by-value decay 成指標;by-reference 保留真正型別 |
| auto vs template 推導 | 唯一差異:auto 把 { ... } 推導為 std::initializer_list |
| auto 回傳型別 / lambda auto 參數 | 走 template 推導 → 拒收 braced initializer |
| decltype | 對名稱如實回報;對非名稱 lvalue 運算式回報 T&(括號陷阱) |
decltype(auto) |
以 decltype 規則推導,保留 reference/const;對括號極敏感 |
| 檢視型別 | 編譯期 TD 模板最準;typeid 必然失真;Boost.TypeIndex 準確;理解 Items 1-3 才是根本 |