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 2auto 推導規則)與 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 並非萬靈丹——兩大例外(詳見 Item 2 與 Item 6)

  1. 推導規則的意外(Item 2):auto x = { 27 }; 推導為 std::initializer_list<int> 而非 int——braced initializer 是 auto 與 template 推導唯一的分歧點;by-value 的 auto 也會如 template 推導般丟棄 reference 與頂層 const。
  2. 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 是選項非強制