2018-01-02

Effective C++

課後複習筆記

Ch1 基本

01:聯邦

  • C
  • 物件導向
  • Template
  • STL

準則就是因地制宜

02:避免 #define

#define ASPECT_RATIO 1.89
const double AspectRatio = 1.89;
  • 編譯器看不到 ASPECT_RATIO
    • 錯誤訊息看不懂
    • 目的碼較大
    • 無作用域、無法封裝
  • static const member
    • 可能需要定義實體
    • 可能無法 in-class 設定初值
    • enum hack
  • MACRO 超雷!! → inline function

03:const

  • pointer/iterator 皆有兩種 const
  • 令 operator 回傳 const 物件
  • const 物件只能呼叫 const method
    • const 也能多載 → 如 operator []
    • physical/logical constness 與 mutable
  • const 與 non-const 間的重複程式碼 → const 者較嚴謹作為 callee

04:初始化

  • array (C lang) 無;vector (C++/STL) 有。
  • primitive type → 請手動;object → 用 constructor。
  • initialize 與 assign 不同,請使用 initialization list。
    • member primitive type → 請手動
    • member object → 隱含呼叫 default constructor
    • 怕忘記就全列上
    • const member, reference 還不得不用!
  • DRY 的權衡
  • 初始化的次序
  • 跨編譯單元初始化次序 (solution: singleton)

Ch2 默默函式

05:默默函式

  1. default constructor (若沒其他 ctor)
  2. destructor (non-virtual by default)
  3. copy constructor
  4. copy assignment

哪時用了?

Foo f;    // ctor/dtor
Foo f(g); // copy ctor
f = g;    // assign

做了些啥?

  • construct/destruct
    • base classes'
    • (non-static) members'
  • copy
    • member primitive type → 複製
    • member object → 呼叫對應函式 (恐遭拒)
    • const member, reference 沒法 copy assign → 自行處理

06:不要默默函式

  • private 且 不予實作
  • (以 private 方式) 繼承 uncopyable

延伸學習:(C++11) = delete;

07:polymorphism + virtual destructor

  • 需要 virtual destructor 的原因 → 衍生成份未能銷毀
    • 繼承 class 時 (尤 STL) 請當心!!
      註:雖可繼承非 virtual dtor 的 uncopyable,但不該用在 polymorphism。
  • 實現 virtual function 的代價
    • 物件大小增加 → vptr
    • 不再是 POD (plain old data)
  • pure virtual destructor 還是需要找個地方實作

延伸學習:virtual table

08:別從 destructor 丟出 exception

離開 scope 就會呼叫 destructor
若拋出異常時又再碰到異常,程式會爆炸 → destructor 必須吞下異常

09:ctor/dtor 不可呼叫 virtual function

在 base class 的 ctor/dtor 裡呼叫 virtual function → 是 base class 的實作
畢竟 derived class 尚未初始化/已完成解構。

  • 特別小心間接的呼叫!!
  • 在 initialization list 使用 private static 輔助函式的技巧

10:operator = 要回傳 reference to *this

  • 以便 x = y = z = 3
  • +=-=*= 也要

11:operator = 的 self assignment

  • 你可能沒想到「自我賦值」會這樣發生
  • 解決技巧
    • 證同測試
    • 精心巧妙的順序
    • copy & swap

12:複製物件所有成分

  • 哪些成分?
    • all members → 尤其增添成員時
    • base classes → 非常容易忘記
  • 哪些函式?
    • copy constructor
    • copy assignment
    • all the other operator =s

copy constructor 不該呼叫 copy assignment,反過來也不行,
應該由第三個函式來消除重複程式碼。

Ch3 資源管理

13:以物件管理資源

不要再寫超級難以維護的程式碼了! 多想幾秒,你可以寫物件。

  • 拿到就托管 (RAII)
  • 利用 destructor
shared_ptr<Jason> pJason(new Jason);

smart pointer

  • auto_ptr 禁止複製,舊值成為 NULL。
  • shared_ptr 可以複製
    • 故可用於 STL
    • 小心 circular reference
  • delete []?
    • 改用 STL
    • boost::scoped_array, boost::shared_array

14:當心複製

總是思考:複製時,該發生什麼事?

  • uncopyable
  • reference count
    • shared_ptr 可利用 deleter 自訂行為
    • 例子中巧妙利用 destructor 行為
  • deep copy
  • transfer
    • auto_ptr

15:要能存取原始資源

總有不完美的時候…

  • 顯式
    • pJason.get()
    • (微隱式) *pJasonpJason->method()
  • 隱式
    • operator TYPE() const {}
    • 固然方便,但易產生資源管理漏洞。

→ 封裝 or not 封裝,自行斟酌。

16:成對使用 new/delete

  • newdelete 所做的事
  • 混用的後果
  • typedef 陣列

17:shared_ptrnew 之間,容不下其他人!

  • explicit constructor 的效用
  • 各引數的執行順序並無規定 → 要是 new 到建構 shared_ptr 之間發生意外!?

Ch4 設計

  • 菜鳥:令軟體做出你希望他做的事情
  • 大師:讓介面容易使用,不容易誤用

目標:正確性、高效性、封裝性、維護性、延展性、一致性

18:避免誤用

  • 客人誤用,你也有責任!
  • Date class 的例子
    • 避免順序錯誤
    • 避免範圍錯誤
  • operator * 回傳 const type
  • STL 皆有 .size() → 婊 Java、嗆 C♯
  • 索性回傳 shared_ptr
    • 避免忘了 delete
    • 避免錯用 delete 方式
    • cross-DLL problem (?
    • shared_ptr 的成本/效益

19:class = type

謹慎、細心斟酌

  • construct/destruct
  • construct/assignment
  • pass by value
  • valid value
  • inherit
  • type cast
    • non-explicit-one-argument constructor
    • operator T
  • interface (public methods)
  • 默默函式
  • public/protected/private, friend
  • time-complexity, exception-safe, thread-safe
  • generalize/template
  • Do you really need a class?

20:pass by constant reference

  • call-by-value 的成本
  • 勿忘 const
  • 物件切割
  • 斟酌
  • 小 class?
    • 程式碼優化
    • 未來可能變大

21:return a reference?

always ask: reference to what?

  • reference to a local (stack) object?
  • reference to a allocated (heap) object?
  • reference to a static object?

延伸閱讀:C++11 move semantic

22:private member

  • getter/setter 寫法
  • 封裝的精神:為實作提供彈性
  • protected 亦同

23:non-member non-friend function 取代 member function

  • 少一個 function 能存取 member
  • namespace 的擴充能力

24:non-member operator

  • 數值型別 (如:大數、有理數) 適合隱式轉換
  • Foo operator *(const Foo &rhs) const;
    • x * 2x.operator *(2) ok (隱式型別轉換:被當參數時才會)
    • 2 * x2.operator *(x) error!!
  • Foo operator *(const Foo &lhs, const Foo &rhs);
    • 且不必為 friend

25:swap

  • swap 用途廣泛:條款 11、條款 29
  • std::swap() 之實作
    • 需可複製:copy construct, copy assignment
    • 有效率問題才自訂 swap
  • pImpl 設計
    • 資料全部放在 FooImpl* pImpl
    • swap 便交換指標即可
  • 背景知識
    • 全特化/偏特化
      • function template 不可偏特化 → 重載即可
    • 擴充 std 命名空間之禁忌 → Extending the namespace std
  • 自訂 swap
    • 全特化 std::swap → 不適用於 class template
    • 以 member swap function 取代 friend function → 循 STL 慣例
    • 實作自己的 swap (利用 Koenig loopup) → 通用
      • 但針對一些 白目,還是得準備 std::swap
      • 最好在自己的 namespace,避免在 global。
  • 使用 swap
    1. using std::swap;
    2. swap(a, b) (赤裸裸的)
  • member swap function 不要拋出異常 → 條款 29
  • C++11: move semantics → stack overflow

Ch5 實作

26:拖延症不是病

  • 建構/解構皆成本 → 不要宣告卻不使用
    • 怎麼可能!?
  • 精髓:賦值 constructor 優於 default constructor + assignment
  • 迴圈內/迴圈外

27:儘量不要轉型

珍惜 C++ 強型別,遠離轉型。

  • 舊式
    • (T)expr (C style)
    • T(expr) (function style)
  • 新式
    • const_cast<T>(expr)
    • dynamic_cast<T>(expr)
    • reinterpret_cast<T>(expr)
    • static_cast<T>(expr)
  • 新式較好
    • 十分醒目:容易發現問題所在
    • 功能窄化:減少不預期的行為
  • 在 explicit constructor 時使用 const_cast
  • 轉型很忙
    • intdouble
    • base *pb = &d; 多重繼承之故,指標可能 offset。
  • 誤用一:(其實不太懂)
    • 正確用法 base::method();
  • 誤用二:dynamic_cast 成本高
    • 直接操作 derived 類別
    • 從 base 使用 virtual function (取捨)
  • 誤用三:連串 dynamic_cast

28:avoid handle to member

  • Content& getContent() const; (×) 可藉 Content& 修改內容
  • const Content& getContent() const; (△) 有 dangling 風險
    • 不過 STL 的 operator[] 也都這樣
    • 使用時盡量降低 dangling 風險
  • handle → reference、pointer、iterator
  • member → 變數、函式

29:異常安全保證

  • 安全
    • 不洩漏資源 → 如:改用 auto lock
    • 無資料敗壞 → 如:真的發生才改變狀態
  • 保證
    • 異常拋出,值仍有效
    • 異常拋出,值仍不變
    • 不拋出異常
  • 實現技巧
    • copy & swap
  • 坨屎壞粥

30:inline

  • pros & cons
    • 無呼叫成本
    • 可供最佳化
    • 目的碼 縮小/膨脹 → paging, instruction cache hit rate (why?),
    • 重新編譯 vs 重新連結
    • 不易 break point (?
  • 寫法
    • 明:inline
    • 暗:在 class 內定義 (含 friend)
  • 必須定義於 header file (的原因)
    • 別與 template 搞混!
  • 編譯器可以拒絕 → 看 warning
    • 太複雜的函式 (迴圈、遞迴)
    • virtual 函式 (runtime 才知道)
  • inline function pointer → 仍產生函式本體
  • inline constructor/destructor → 因為其實做了很多事 (即便看來是空的),可能不適合。

31:compile dependency

  • 目標:唯有介面改動才慘烈重編 → 給外人知道越少越好
  • 介面/實作
    • C++: 要配置多少記憶體空間??
    • Java: 全部指標
  • 方法一:pImpl
    • 前置宣告
  • 方法二:abstract base class
    • 記得 virtual destructor
    • factory
  • 成本

Ch6 物件

32:public 繼承 = is-a

最重要的條款

  • 所有 B 可派上用場之處 D 也可以
  • 例一:鳥會飛?
    1. 飛鳥/非飛鳥 → 繼承體系混亂
    2. 企鵝可以飛,只是會拋出錯誤 → 執行期間會才知道
    3. 嚴格說來鳥不應具飛的函式
  • 例二:正方形是矩形? → 加寬 可否適用於正方形?

33:避免遮掩繼承而來的名稱

  • 與 global/local scope 的道理相同 型態不同也會遮蔽 (無論變數或函式)
  • 重載函式 ← 問題所在
    • using
    • inline 轉交

34:繼承介面 & 繼承實作

  • pure virtual 只繼承介面
    其實仍可提供實作
  • impure virtual 繼承介面、預設實作
    可能忘記自訂實作
    • 可改用 pure virtual + (protected) default implement → 強迫明確聲明 (inline?)
    • 或乾脆 pure virtual 並提供實作
  • non-virtual 繼承介面、不准改變實作

務必審慎評估使用

35:virtual 以外的方法

請多多比較各種方法

  • non-virtual interface (NVI)
    • 該流派:virtual 應該總是 private
    • 可進行 事先/事後 處理
    • 重新定義繼承來的 private virtaul 是 OK 的
  • function pointer
    • 相同 class 可用不同 function;不同 class 可用相同 function。
    • 可動態替換
    • 只能從 public 介面存取主物件
  • function object
    • 更彈性於 function pointer
    • 搭配 bind 更強大
  • 傳統 strategy
    • 可擴充 base

36:別覆寫繼承來的普通函式 (non-virtual)

  • 由於靜態綁定,即便是同一物件,行為會受 pointer/reference 型別不同而有所不同。
  • 理論上若真要如此就該 virtual

37:別改變繼承來的預設參數

  • 繼承 non-virtual function (x);繼承 virtual function (o) → 條款 36
  • 動態綁定 & 靜態型別
    • virtual function 動態綁定;預設參數卻根據靜態型別。
    • Why? 比較簡單比較快
  • 維護相同預設參數很麻煩 → NVI

38:複合 = has-a 或 據該物實作

重點不是區分 has-a 與 據該物實作,而是區分兩者與 is-a。
而 據該物實作 尤其容易與 is-a 混淆

例:NuThread、SocketHelper

39:private 繼承

  • 行為
    • 不自動轉型為 base
    • 繼承來的都是 private
  • 意義:據該物實作 (繼承實作,不要介面)
    • 是的,與 複合 相同
    • 盡量使用 複合
  • 必要才用 private 繼承
    • 存取 protected 成員
    • 重新定義 virtual 函式 → 總能改用 複合
    • empty base optimization

40:多重繼承

  • 兩個 base 的成員有一樣的名稱?
    • 先最佳匹配,才檢查可用性。 (最配卻不可用)
    • ambiguous!
    • 明確指出以解決歧義:mp.BaseA::foobar()
  • 鑽石型多重繼承 (virtual base class)
    • 共同的 base 可以共用一份 → class foo : virtual public bar {};
    • virtual 繼承的成本
      • 總需透過指標參照 ① 耗用一個指標的空間 ② dereference 指標的時間
      • 初始化順序較為複雜 → 由最底層負責
    • 非必要別用,或別在 virtual base 中放置資料。
  • 最後提供一個例子:class CPerson : public IPerson, private PersonInfo {};

Ch7 TEMPLATE

41:隱式介面/編譯期多型

  • 物件導向:顯式介面、執行期多型 → 明確由函式簽名式取決
  • 泛型編程:隱式介面、編譯期多型 → 憑具現化能否成功取決

42:typename

template <class T> ...
template <typename T> ...

以上兩者意思相同

dependent name 問題

T::iterator * x

  • 是 型別 還是 變數?
  • 碰到 dependent name 就以 typename 標明
  • 例外:typename 不可出現在…
    • base class list
    • initialization list
  • 若 dependent name 太長可以先 typedef 起來
    如:typedef typename std::iterator_trait<T>::value_type value_type;

43:來自 template base class

template <typename T>
class my_base {};

template <typename T>
class my_class : public my_base<T> {};
  • 繼承 template class 時,若未知其確切型別,則無法得知繼承而來的成員是否存在。
    • 因為可能特化 (CompanyZ 為例)
  • 解法 → 承諾它在
    • this->
    • using
    • my_base<T>::
  • 承諾未兌現 → 仍將編譯失敗
  • 及早報錯:解析 template → 具現化 template

44:留意程式碼膨脹

  • template:寫一次,具現化多次 (用到才具現化)
  • 抽出相同部份 → template 恐因具現化多次而程式碼膨脹
template <typename T, size_t N>
class matrix {};

matrix<int, 3> p;
matrix<int, 4> q;
matrix<int, 5> r;
  • 解法
    • 共用函式 → 置於 base class
    • 避免反覆傳遞參數 → 記為 member (斟酌 protected)
  • 優劣分析
  • 膨脹現象在 tyepname Tsize_t N 兩種參數皆存在

45:member function template

  • smart pointer:以物件仿造指標,以提供額外功能。
    • auto_ptrshared_ptr 自動釋放資源
    • iterator 移至下個節點
  • 別忘仿造 隱式轉換 ① base/derived ② const/non-const
    • B* p = new D;
    • smart_ptr<B> p = smart_ptr<D>(new D); → 問題在 smart_ptr<B>smart_ptr<D> 完全無關
  • How?
    • member function (此為 copy ctor) template (拜託不要列舉
    • 不需 explicit
    • 限制轉換對象:交由原始指標處理
  • shared_ptr 為例
    • 不限於 ctor
    • 額外支援 weak_ptrauto_ptr (但 explicit)
    • auto_ptr 的 reference 並不為 const
  • 仍要防止默默函式默默產生

46:non-member operator (template ver.)

  • 延伸 條款 24,在 template 版本,x * 2 便不能成立。
  • 因為 template 引數推導的過程不考慮隱式轉換 (該函式尚未存在…)
  • 解法:寫在 class template 內 (成為 friend function) 可避開 function template 特有的引數推導
    • friend? 其實不打算存取 private 成員啦~
    • class template 速記式:foo<T>foo
  • link error? 直接實作在 class template
  • inline? 不然再轉呼叫 (雖仍必須實作於標頭檔,但不再是 inline 了)

47:trait

5 種 iterator

category R/W move inherit
input R×1 ++ -
output W×1 ++ -
forward ++ input
bidirectional ++/-- forward
random access bidirectional
  • 怎麼讓 std::advance 盡量使用 random access → 如何對型別挾帶資訊?
  • nesting information (my_iterator::categroy) 不適用於內建型別 (char * 也應該要可以
  • trait:讓額外的型別挾帶資訊
    • 自訂型別:iterator_trait<my_iterator>::category → 仍轉交 nesting information
      ​​​​template <typename T>
      ​​​​struct iterator_trait
      ​​​​{
      ​​​​    typedef typename T::category category;
      ​​​​}
      
    • 內建型別:iterator_trait<char *>::categroy → 偏特化方式聲明
      ​​​​template <typename T>
      ​​​​struct iterator_trait<T*>
      ​​​​{
      ​​​​    typedef foobar_iterator_tag category;
      ​​​​}
      
  • compile time 的 if-else:overloading
  • iterator_trait 的其他資訊;其他的 trait。

48:超編程!

  • 在 compile time 就完成 runtime 的事
    • 提早發現問題
    • 更小的執行檔;更短的執行期;更少的記憶體。
    • 花更久的時間編譯
  • 條款 47 (typeid/trait) 便是一例
  • 什麼都可以算 → boost MPL 提供高階語法
  • TMP 入門:計算階乘
    • 以遞迴 (具現化) 實現迴圈
    • 以特化為終止條件
  • 實務應用

Ch8 NEW/DELETE

49:new handler

  • new 不出來…
    • 曾經: return NULL
    • 現在: throw std::bad_alloc
  • std::set_new_handler
    • 不斷呼叫該 handler 直到成功
    • 小心 infinite loop/recursive
  • handler 可以…
    • 騰出空間
    • 去找其他 handler
    • 裝死:set_new_handler(nullptr)
    • std::bad_alloc
    • 自爆:abortexit
  • nothrow new
    • 你 nothrow 不代表別人要 nothrow 啊!!
    • 所以別再用了

令不同 class 呼叫不同 handler

  • 沒有內建,所以自幹
    • 提供 .set_new_handler()
    • operator new 偷偷 set_new_handler
    • 記得恢復 → 以物件管理資源
  • 不妨寫成公用 base class
    • 為何 template? 每個 class 要有自己的 static variable
    • CRTP (curiously recurring template pattern) = do it for me

50:啥時自幹 new/delete

Why?

  • 檢查誤用
    • 忘 delete (memory leak) 或 delete 多次 (不確定行為)
    • overrun 或 underrun → 藉 signature
  • 要快
    • 要通用就沒有針對優化
    • 可針對 固定尺寸、單執行緒 等
  • 要省:省掉 cookie
  • 統計
    • 大小、壽命、次序、變化 → 知道了才好優化吧!
  • alignment:也許編譯器不好
  • 避免 page fault
  • 特殊行為:share memory、資料抹去

How?

void* operator new(std::size_t size) throw(std::bad_alloc) {...}
  • 應反覆呼叫 new handler → 條款 51
  • alignment! alignment! alignment! → 否則 變慢 或甚至 硬體異常
  • 其實 debug 下多已內建

51:自幹 new/delete 的 SOP

(global/member) operator new

  • 成功:回傳指標 (指向該記憶體)
  • 失敗:
    • 有 new handler 就 call (現在有 get_new_handler 可用了
    • 沒就 throw std::bad_alloc
  • 要求 0 byte:也須給合法指標 → 給它 1 byte
  • 考慮被繼承的狀況:若 size != sizeof(base) 便改用標準 operator new (要是衍生類別的大小恰巧一樣!?
    • 獨立物件的大小不得為零

(global/member) operater delete

  • 確保 delete null 指標安全
  • 對應的 size != sizeof(base)

(global/member) operator new[]

  • 元素的個數與大小皆無法斷定 → 所以好像沒啥好做的
  • 可能包含額外空間存放元素個數

題外話:base 要有 virtual dtor

52:placement new

  • placement new 是指…
    • 狹義:void* operator new(std::size_t, void*)
    • 廣義:任何具額外參數的 operator new
  • p = new (true) foobar;
    • void* operator new(std::size_t, bool)
    • constructor
  • 要是 constructor 失敗,C++ runtime 會…
    • 呼叫對應的 void operator delete(void*, bool)
    • 但要是沒有就算了 → 造成 memory leak
  • 要是使用者 delete p; (而非 delete (true) p;
    • 所以也要提供正常版 operator delete (不算使用者誤用嗎??
  • placement new/delete 會遮掩正常版 new/delete (同理於條款 33)
    • 可提供內含全部正常版 new/delete 的 base class 以便於 using

Ch9 雜項

53:重視警告

  • 警告背後的成因也許與字面上不同
  • 也勿過於依賴警告 (它也許不警告
  • 爭取 0 警告

54:熟悉標準函式庫

cppreference.com

55:熟悉 boost

The Boost C++ Libraries