Item 5:優先使用 auto 而非顯式型別宣告 (Prefer auto to Explicit Type Declarations)
Overview Table
| 面向 | 顯式型別宣告 | auto 宣告 |
|---|---|---|
| 初始化 | 可能忘記初始化(值不確定) | 強制初始化,否則編譯錯誤 |
| 冗長度 | typename std::iterator_traits<It>::value_type |
auto currValue = *b; 一行搞定 |
| closure 型別 | 無法寫出(只有編譯器知道),只能退用 std::function |
直接持有 closure 本身的型別 |
| type shortcuts | unsigned sz = v.size(); 有 32/64 位元移植性風險 |
auto sz = v.size(); 精確得到 size_type |
| 隱式轉換 / 暫時物件 | 型別不符時默默複製出 temporary | 型別與初始化運算式一致,零多餘轉換 |
| 重構 | 改回傳型別後要逐一修改呼叫端 | 重新編譯即自動跟著更新 |
| 打字量 | 多 | 少 |
核心結論(Things to Remember)
auto變數必須初始化、大致免疫於導致移植性或效率問題的型別不符 (type mismatches)、利於重構、且通常打字更少。auto變數仍受 Item 2(auto推導規則)與 Item 6(invisible proxy type)所述陷阱影響。
auto 解決的三大宣告痛點
int x1; // 可能未初始化,值不確定(依語境而定)
auto x2; // 編譯錯誤!auto 必須有 initializer 才能推導
auto x3 = 0; // OK,x3 的值明確定義(int)
// 痛點 2:冗長的 iterator value type
template <typename It>
void dwim(It b, It e) {
while (b != e) {
// C++98 寫法:typename std::iterator_traits<It>::value_type currValue = *b;
auto currValue = *b; // C++11:一行解決,語意相同
// ...
}
}
// 痛點 3:closure 的型別只有編譯器知道,根本寫不出來
auto derefUPLess = // C++11:比較
[](const std::unique_ptr<Widget>& p1, // std::unique_ptr
const std::unique_ptr<Widget>& p2) // 指向的 Widget
{ return *p1 < *p2; };
auto derefLess = // C++14:lambda 參數
[](const auto& p1, const auto& p2) // 也可用 auto,泛化到
{ return *p1 < *p2; }; // 任何 pointer-like 型別
| 痛點 | 顯式宣告的問題 | auto 的解法 |
|---|---|---|
| 未初始化變數 | int x; 值不確定 → 錯誤溫床 |
initializer 是語法必需品 |
| 冗長型別 | iterator_traits 咒語難記難讀 |
由 initializer 推導(規則見 Item 2) |
| 編譯器專屬型別 | closure 型別無法手寫 | auto 可表示只有編譯器知道的型別 |
持有 closure:auto vs std::function
std::function 是 C++11 標準庫中泛化 function pointer 的模板,可指向任何 callable;但拿它存 closure 與 auto 完全不等價:
| 比較項 | auto 變數 |
std::function 變數 |
|---|---|---|
| 型別 | 就是 closure 本身的型別 | std::function 模板實體(固定簽章) |
| 記憶體 | 只佔 closure 實際所需 | 固定大小;裝不下時 heap 配置,通常較大 |
| 呼叫速度 | 直接呼叫、可 inline | 間接呼叫、抑制 inlining,幾乎必然較慢 |
| 例外風險 | 無額外配置 | 可能拋 out-of-memory |
| 書寫成本 | auto 四個字母 |
須重複寫出完整函式簽章 |
// std::function 版:又長、又大、又慢
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };
// auto 版見上節 —— game, set, and match for auto
持有
std::bind 結果的比較同樣是 auto 勝出;但 Item 34 建議根本改用 lambda 取代 std::bind。type shortcuts:顯式型別引發的隱性錯誤
案例一:移植性炸彈
std::vector<int> v;
unsigned sz = v.size(); // 官方回傳型別是 std::vector<int>::size_type
// 32-bit Windows:unsigned == size_type(皆 32 bits)→ 沒事
// 64-bit Windows:unsigned 32 bits、size_type 64 bits → 可能截斷!
auto sz2 = v.size(); // sz2 必為 std::vector<int>::size_type,移植無虞
案例二:看不見的 temporary copy
std::unordered_map 的 key 是 const,元素真正型別是 std::pair<const std::string, int>:
std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m) { /* ... */ }
// 錯!宣告型別與元素型別不符 → 每個元素被複製成暫時物件,p 綁到 temporary
for (const auto& p : m) { /* ... */ }
// 對:p 直接綁定 m 中元素;取 &p 保證得到指向容器內部的指標
宣告 const std::pair<string,int>&(型別不符):
m 的元素 暫時物件 p
pair<const string,int> --複製--> pair<string,int> <--綁定-- const&
(原件) (每圈迭代結束即銷毀!)
↑ 取 &p 會拿到懸空指標
宣告 const auto&(型別必然吻合):
m 的元素 <---------- 直接綁定 ---------- p (零複製、無暫時物件)
共同教訓:顯式指定型別常引入你既不想要也未預期的隱式轉換;用 auto 就不必擔心「宣告的型別」與「初始化運算式的型別」不一致。
可讀性疑慮與重構優勢
auto是選項不是強制:若顯式型別讓程式更清楚、更好維護,仍可自由使用。- type inference 並非 C++ 獨創:C#、D、Scala、Visual Basic 及 ML、Haskell、OCaml、F# 等靜態型別語言早已採用,業界已證明它與大型工業級程式庫的開發維護毫不衝突。
- 可讀性緩解:IDE 可顯示推導型別(惟注意 Item 4 提到的 IDE 顯示限制);配合良好命名,「這是容器 / 計數器 / smart pointer」等抽象層級的型別資訊通常已足夠。
- 重構優勢:函式回傳型別從
int改為long時,存入auto變數的呼叫端重新編譯即自動更新;顯式宣告int的呼叫端則須人工逐一修改。
auto 並非萬靈丹——兩大例外(詳見 Item 2 與 Item 6)
- 推導規則的意外(Item 2):
auto x = { 27 };推導為std::initializer_list<int>而非int——braced initializer 是auto與 template 推導唯一的分歧點;by-value 的auto也會如 template 推導般丟棄 reference 與頂層 const。 - invisible proxy type(Item 6):
auto highPriority = features(w)[5];會推導出std::vector<bool>::reference而非bool,可能持有懸空指標導致 undefined behavior;解法是 explicitly typed initializer idiom:auto highPriority = static_cast<bool>(features(w)[5]);。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
auto x; 沒有 initializer |
編譯錯誤——auto 強制初始化,根絕未初始化變數問題 |
持有 lambda closure:auto vs std::function |
auto:型別即 closure、較小較快、可 inline;std::function 較大較慢、可能 heap 配置與 out-of-memory |
unsigned sz = v.size(); 移到 64-bit Windows |
type shortcut 陷阱:unsigned 32 bits、size_type 64 bits → 可能截斷;改 auto |
for (const std::pair<std::string,int>& p : m) 遍歷 unordered_map |
元素實為 pair<const string,int> → 每圈複製出暫時物件,&p 是懸空指標;改 const auto& |
| 函式回傳型別改變後,呼叫端要不要改? | 存入 auto 變數者重新編譯自動更新——auto 利於重構 |
auto 一定推導出你要的型別嗎? |
否——braced initializer(Item 2)與 invisible proxy type(Item 6)是兩大陷阱 |
| 「看不出型別,可讀性差」的反駁 | IDE 顯示型別 + 良好命名提供的抽象型別資訊通常已足夠;auto 是選項非強制 |
Related Notes
- 03-Auto/02-Explicitly-Typed-Initializer-Idiom — 同資料夾兄弟筆記(Item 6):
auto推導出 invisible proxy type 時的修正手法 - 02-Deducing-Types/02-Auto-Type-Deduction — Item 2:
auto推導規則本體,含 braced initializer 的特殊行為 - 02-Deducing-Types/04-Viewing-Deduced-Types — Item 4:如何檢視
auto實際推導出的型別(IDE / 編譯器診斷) - 07-Lambda-Expressions/04-Lambdas-Vs-Std-Bind — Item 34:
auto持有 closure 優於std::function的論證同樣適用於std::bind,且 lambda 優於std::bind - 03-Auto/Practice-Auto — 本章練習題