Scott Meyers:more Effective C++——35 New Ways To Improve Your Programs And Designs@2008

《More Effective C++: 改善程式與設計的 35 個具體做法》核心論點闡述

本書旨在提供 C++ 程式設計師更深入、更進階的指導,超越基本的語法和特性介紹,著重於如何寫出更健壯、更高效、更易於維護和擴展的 C++ 程式碼。透過檢視語言的細微之處以及特性之間的互動,本書提出了 35 個具體的「做法」(Item),旨在幫助開發者改進他們的程式和設計。核心論點圍繞幾個關鍵主題:

1. 深刻理解 C++ 核心機制的細微差異與影響

本書強調,C++ 的許多基本機制看似簡單,但在實際應用中存在重要的細微差異,這些差異對程式的行為、效率和正確性有深遠影響。例如:

  • 指標(Pointers)與參考(References)的區別(Item 1): 雖然都可以間接引用物件,但指標可以為空(null)且可重新指向不同物件,而參考必須初始化且始終引用同一物件。這決定了它們各自的適用情境:需要表示「沒有物件」或「可能引用不同物件」時使用指標;確保始終引用一個物件且引用關係不變時使用參考,特別是在重載 operator[] 等需要返回可賦值目標的運算符時,參考是首選。理解這些差異對於避免空引用錯誤、確保物件始終有效以及實現特定運算符語法至關重要。
  • C++ 風格的型別轉換(C++-style casts)優於 C 風格的轉換(Item 2): C 風格的型別轉換 (type)expression 語法粗糙且難以搜尋和區分其意圖。C++ 引入了 static_castconst_castdynamic_castreinterpret_cast 四種新的轉換操作符。這些新操作符不僅提高了程式碼的可讀性和可搜尋性,更重要的是,它們能明確表達轉換的意圖(如移除常數性、在繼承體系中安全轉換、低層次位元重新解釋),並允許編譯器進行更嚴格的檢查,診斷 C 風格轉換無法捕捉的錯誤。應優先使用 C++ 風格的轉換以提高程式碼的安全性和清晰度。
  • 理解 newdelete 的不同含義(Item 8): newdelete 是內建於語言的操作符(operators),負責分配記憶體 呼叫建構函式/解構函式。而 operator newoperator delete 是函數,由 newdelete 操作符呼叫,僅負責記憶體分配/釋放。理解這兩者的區別至關重要:若只想分配未初始化的記憶體,應直接呼叫 operator new;若要建立物件,必須使用 new 操作符;若要自訂記憶體管理行為,應重載 operator new/operator delete 函數。對於陣列,有對應的 operator new[]/operator delete[]。這種區分是實現記憶體池、物件定位(placement new)等進階技術的基礎。

2. 警惕並管理隱藏的成本與潛在錯誤

某些語言特性或常見的程式設計模式會引入不易察覺的成本或潛在的錯誤來源,需要程式設計師主動識別和避免。

  • 謹慎使用使用者定義的隱式轉換(Item 5): 單參數建構函式和型別轉換運算符(如 operator double()) 允許編譯器在需要時自動進行型別轉換。這雖然提供了便利,但可能導致編譯器在非預期的地方插入隱式轉換,呼叫了錯誤的函式,或產生了不必要的臨時物件,從而引入難以追蹤的邏輯錯誤或效能問題。應優先使用 explicit 關鍵字修飾單參數建構函式,並將型別轉換運算符替換為普通的命名函式(如 asDouble()),迫使客戶端顯式呼叫轉換,以提高程式碼的清晰度和安全性。
  • 絕不重載 &&||,(Item 7): 這些運算符對於內建型別具有特殊的求值語義:&&|| 具有短路求值(short-circuit evaluation)且參數求值順序固定(從左到右);, 運算符保證左側先求值後再求值右側。當重載這些運算符時,它們變成了普通的函式呼叫,失去了原有的特殊語義:所有參數都會被求值,且參數求值順序不再保證。這會破壞程式設計師對這些運算符的固有假設,導致依賴這些語義的程式碼行為異常或錯誤。
  • 理解例外處理的成本(Item 15): 例外處理機制(try, throw, catch)雖然強大,但會引入額外的執行時成本,即使程式碼中沒有實際拋出例外。這些成本包括維護物件建構狀態、跟蹤 try 區塊、查找 catch 子句所需的資料結構及執行時檢查。try 區塊和例外規範(exception specification)也會增加程式碼大小和執行時間。實際拋出例外的成本更是遠高於普通函式返回。應將例外保留給真正「例外」的情況,而非用於流程控制或表示常見錯誤。必要時,應對程式碼進行效能分析以評估例外處理的實際影響。

3. 優先使用基於資源管理物件的慣用法

C++ 提供了強大的機制來自動管理資源(如記憶體、檔案控制代碼、鎖等),這對於在存在例外或其他非線性控制流(如函式返回)的環境中防止資源洩漏至關重要。

  • 利用解構函式防止資源洩漏(Item 9): 核心思想是「資源取得即初始化」(RAII)。將資源的管理(分配和釋放)封裝在物件中,資源的分配在物件的建構函式中完成,資源的釋放在物件的解構函式中完成。由於局部物件在函式退出時(無論是正常返回還是拋出例外)會自動呼叫解構函式,這保證了資源總能被正確釋放。std::auto_ptr(儘管有其限制,如所有權轉移)就是一個典型的例子,它在解構函式中自動刪除其指向的動態分配的記憶體。這比手動 delete 或依賴 try-catch-finally(C++ 沒有 finally,但 catchthrow 可模擬)更安全可靠。
  • 在建構函式中防止資源洩漏(Item 10): 若物件的建構函式中分配了多個資源,且在分配某個資源時拋出例外,那麼建構函式中之前已成功分配的資源可能因物件未完全建構而無法呼叫其解構函式,導致洩漏。解決方案是在建構函式內部捕獲例外,在重新拋出之前清理已分配的資源。更好的方法是將建構函式中分配的資源封裝在具有 RAII 特性的成員物件中(例如,將原始指標成員替換為 std::auto_ptr 或其他智慧指標),這樣即使建構函式拋出例外,這些已成功建構的成員物件的解構函式也會被呼叫,自動清理其管理的資源。

4. 妥善處理運算符重載中的細節

重載運算符應當遵循與內建型別相似的行為模式,並注意其效能影響。

  • 區分前置(prefix)與後置(postfix)形式的增量與減量運算符(Item 6): ++-- 運算符的前置形式(++i)和後置形式(i++)在語義上不同(前置是「先增/減後取」,後置是「先取後增/減」),且在 C++ 中可以區分重載(後置形式帶一個無用的 int 參數)。重要的是,它們的返回型別不同:前置形式應返回對物件本身的參考(T&),後置形式應返回修改前的物件副本(const T)。為了避免不必要地複製物件,前置形式通常比後置形式更有效率,應盡量使用前置形式。後置形式的實現應基於前置形式,以保證行為一致性,並返回 const 物件以防止連鎖的後置操作(如 i++++)。

5. 認識並善用進階設計模式與技術

本書還介紹了一些 C++ 中用於實現特定設計目標的進階技術和模式。

  • 虛擬建構函式(Virtual Constructors)與非成員函式(Item 25): 雖然 C++ 沒有真正的「虛擬建構函式」(物件尚未存在,何談虛擬呼叫?)或「虛擬非成員函式」,但可以通過特定的模式來模擬類似的行為。例如,「虛擬複製建構函式」(virtual copy constructor,通常命名為 clone())是一個虛擬成員函式,它返回呼叫該函式的物件的副本,允許通過基類指標或參考複製實際派生類型的物件。對於需要根據一個或多個物件的動態型別來決定呼叫哪個非成員函式時(即多重分派,multiple dispatch),可以通過結合虛擬成員函式和非成員函式來實現類似的效果(非成員函式內部呼叫虛擬成員函式)。
  • 代理類別(Proxy Classes)(Item 30): 代理類別的物件充當另一物件的替身。它們可以用於實現一些 C++ 本身不直接支援的語法或行為,或在執行時區分不同的使用情境。典型的應用包括實現多維陣列的 operator[][] 語法(operator[] 返回一個代理物件,該物件再提供另一個 operator[])以及區分 operator[] 的讀取(rvalue)和寫入(lvalue)使用,從而在寫入發生時才執行複製操作(copy-on-write,如在引用計數的字串類別中),實現懶惰求值以提高效率。
  • 引用計數(Reference Counting)(Item 29): 引用計數是一種通過記錄有多少個指標或物件共享同一個底層數據來管理資源(特別是動態分配的記憶體)和提高效率的技術。當引用計數降為零時,表示沒有任何物件再使用該數據,可以安全地釋放。這是一種簡單的垃圾收集形式,可以自動清理不再使用的資源。通過將引用計數和相關邏輯封裝在一個基類(如 RCObject)中,並使用一個知道如何操作引用計數的智慧指標類別模板(如 RCPtr),可以將引用計數的功能添加到任何類別中,從而簡化客戶端的程式碼並減少資源洩漏的風險。

6. 程式設計應著眼未來,考慮可擴展性與維護性

好的 C++ 程式碼應該預見並適應未來的變化,降低維護成本。

  • 預見未來的程式設計(Program in the future tense)(Item 32): 程式設計應考慮未來可能出現的需求、新的派生類別、新的應用場景等。這意味著在設計類別時,即使當前不需要,也要考慮將來可能成為虛擬的函式宣告為虛擬的(如果語義上合理);為類別提供完整的成員函式集(建構函式、解構函式、複製建構函式、賦值運算符),即使當前某些操作看似不使用;通過語言特性(如私有建構函式、私有賦值運算符)來表達和強制執行設計約束(如禁止派生、禁止複製);以及設計易於理解和修改的程式碼。
  • 使非葉子(non-leaf)類別成為抽象類別(Item 33): 在繼承體系中,如果一個類別被設計用作基類(即非葉子類別),即使它當前可以被實例化為具體物件,也應考慮將其設計為抽象類別。這可以防止通過基類指標或參考進行同類型對象的賦值操作時可能導致的「部分賦值」或意外的異質賦值問題。通過引入一個抽象基類,可以迫使程式設計師明確識別和形式化繼承體系中重要的抽象概念,即使在當前這些概念只對應一個具體實現。這有助於改進設計的清晰度和未來擴展性,並能簡化某些運算符(如賦值運算符)的正確行為實現。

總結來說,《More Effective C++》的核心論點圍繞著 C++ 特性的深度理解、基於 RAII 和物件封裝的安全程式設計、效率優化、進階慣用法和設計模式的應用,以及面向未來的可維護和可擴展程式碼設計。書中通過具體的 Items,提供了實用的指導和技術,幫助程式設計師從「能讓程式跑起來」的階段,提升到「寫出好的 C++ 程式碼」的境界。