Try   HackMD

2020-10-03

Effective Modern C++

導讀

  • C++11/14 改變非常多
  • 不僅介紹功能 (到處都有),更要有效使用 (需要經驗)。
  • 條款:原則一定有例外,請詳閱背後推論過程。

術語

  • C++98/03、C++11/14
  • lvalue/rvalue:(通常) 能取得位址者為 lvalue
    • Widget&& w:rvalue reference 型別的變數本身是 lvalue
  • lhs (left-hand side)、rhs (right-hand side)
  • … (刪節號)、... (原始碼)
  • copy:lvalue (copy constructor)、rvalue (move constructor)
  • argument/parameter
  • 例外安全
  • function/callable object、lambda、closure
  • 宣告/定義
  • 函式簽章 (signature)、deprecate、undefined behavior
  • raw/smart pointer
  • ctor/dtor

Ch1 型別推導

  • 推導的地方變多,規則也變多了。
  • 修改一處,擴散至全部。

01:從 template 認識 auto

template <typename T>
void f(P param);

f(expr);

expr 推導出 PT

  • PT&/T*
    • expr 具參考則忽略
    • exprconst (而 P 本身沒註明的話),T 則推導出 const
    • 沒啥意外的
  • PT&&
    • 可想成 lvalue/rvalue 通用的 reference → 條款 24
  • PT (call by value)
    • expr 具參考同樣忽略
    • exprconst 也忽略 (volatile 也忽略)
    • 畢竟複製了
    • 指標所指向的 → 保留 const;指標本身 → 忽略 const (因為複製了)
  • 陣列 衰退成 (指向首個元素的) 指標
    • f(T)char * (無法宣告真正的陣列做為參數)
    • f(T&)char (&)[16] (卻能有真正陣列的參考)
    • 酷技巧:得到陣列元素個數 (但還是盡量以 std::array 取代)
  • 函式 衰退成 函式指標 (沒啥)

02:auto 型別推導

  • 同 條款 01,auto 扮演 T 的角色。
  • 唯一例外:auto x = { 27 } 並非 int x = { 27 }
    • 因為 {} ~= std::initializer_list,便推導成 std::initializer_list<int>
    • 異於 autotemplate <typename T> void f(T param); 無法自 f({ 1, 2, 3 }) 推導。
  • (C++14) 回傳值推導、lambda 參數推導,卻採用 template 方式。
    • auto f() { return { 1, 2, 3 }; } 無法推導
    • auto f = [](auto& param) { … }; f({ 1, 2, 3 }); 無法推導

03:decltype

  • 用以表示該物之型態
  • 使用例:無法確定 template function 回傳值型態
    • auto f(Container& c, Index i) -> decltype(c[i]) (trailing return type 語法,不是推導。)
    • (C++14) 回傳值推導 不見得適用 (auto 套用 T 規則而非 T&) → 以 decltype(auto) 解決
    • 想令 Container 傳入 rvalue? T&& (universal reference) + std::forward (??!)
  • decltype(x)intdecltype((x))int&

04:求救,怎知推導成啥?

  • 靠 IDE
  • 編譯期:template<typename T> class TypeDetect; 小技巧
  • 執行期:typeid(x).name() (不一定可靠)

Ch2 AUTO

  • 寫得快又不出錯,也免於效能問題。
  • 偶爾須引導其得出預期結果

05:auto

  • 避免變數未初始化
  • 省去繁瑣型別名稱 (如 std::iterator_traits<It>::value_type 迭代器)
  • 直接保存 closure (真實型別僅編譯器知道)
    • (C++14) 參數也可 auto
    • std::function 不同於 closure
      • 設計用於任何 可呼叫物件
      • 語法較瑣碎
      • 需配置記憶體
      • 間接呼叫,無法 inline。
  • 殘念,猜錯了!
    • unsigned size = v.size() → 在 64-bit 被截斷
    • const pair<string, int>& (但其實是 const string) → 自動轉型、效能減損
  • 疑慮:不知明確型別
    • 有個概念就夠
    • IDE 也多能提供
  • 更容易重構

06:auto + proxy

  • auto 碰到 proxy 會出問題 (如 vector<bool>::operator[])
  • 與其隱式轉換,不如明確 static_cast

Ch3 酷炫新功能

07:ctor ()/{}

  1. foo x(0); ctor
  2. foo x = 0; ctor (implicit),但常被誤認為 assign。
  3. foo x{ 0 }; 通用初始化
    foo x = { 0 }; 亦同
  • 通用
    • 直接指定容器元素
    • non-static member 從 C++11 開始可以
    • uncopyable
  • 特性
    • 不允許 向下轉型
    • 免受 (most vexing parse) 誤解為 函式宣告
  • 但是 {} ~= std::initializer_list
    • auto x{ 0 }initializer_list
    • 若有 initializer_list ctor
      • 能成功轉出 → initializer_list
      • 需向下轉型 → 不允許
      • 若無法轉型 → 才考慮其他 ctor
  • 特例:foo x{}!?
    • 表示 default ctor
    • 空的 std::initializer_list 呢?? → foo x({})foo x{{}}
  • 使用準則:() 優先 或 {} 優先
  • 寫 template 的兩難

08:nullptr

  • 0int 不是指標;NULL 也不是指標。
    💬 在 C++,任何指標皆可轉型成 void*,但反方向不行,故不將 NULL 定義為 (void*)0
  • 同時多載 整數 與 指標 造成問題
  • nullptr 的型別是 std::nullptr_t
    • 自動轉型成任何 原始指標
    • 程式也更加明確
  • 例子:樣板函式以 0NULL 為引數而推導錯誤

09:using x = y

  • 比較好讀一些 (如 function pointer)
  • 支援 alias template ← 重點
  • 之前的慘況
    • 須宣告為 my_struct<T>::type
    • 若在 template 中要再加 typename
  • <type_traits>
    • 在 C++14 才改用新式
    • 於 C++11 可簡單仿造

10:enum class

  • scoped enum
    • 一般來說 C++ 遇 {} 產生 scope → 但 enum 卻不會
    • enum class 產生 scope,使用必須注明 (color::red)。
  • 轉型
    • enum 能隱式轉換 → 並不好
    • enum class 必須顯式轉換
  • underlying type
    • 若型別未知,則無法 forword-declear。
      • 編譯器也許想自行選擇 時間/空間 最佳化
      • 新增項目時,不該全部重新編譯。
    • enum class 型別預設是 int
      • 可自行指定:enum class color : uint32_t {…}
      • (C++11) enum 亦可
  • underlying_type_t 的例子:std::get<1> 搭配 enum class 使用

11:= delete

  • 不給呼叫函式?
    • 不宣告可以嗎? 默默函式 會自動產生 (以 basic_io 為例)
    • C++98:private 且 不予實作
    • C++11:public= delete
      • 因為 private 而無法呼叫? 要有明確錯誤訊息,故 public

好處都有啥?

  • 不會等到連結才知道
  • 所有函式皆可用
    • 避免不應該的函式多載
    • 避免不應該的樣板產生 → 這在 C++98 完全做不到 (層級不同?)

12:override

  • override 易誤寫成 overload,條件:
    • 對應 method 必須是 virtual
    • 名字
    • 參數/參數型別
    • 回傳值型別
    • const 與否
    • 例外規格 都要相同
    • (C++11) 還有 參考限定符 (鮮為人知的功能)
  • 靠 (不一定會有的) 警告訊息 不如加上 override → 覆寫失敗必定報錯
    • 基底虛擬函式修改時,可知在衍生類別的確切影響。
  • 是 contextual keyword 不怕撞名
  • 參考限定符 的使用場合

13:const_iterator

  • (C++98) 以往 const_iterator 並不堪用
  • (C++11) .cbegin()/.cend() 便於 non-const 容器取得 const_iterator
  • (C++11) begin()/end() 便於泛型最適
    • C++14 才支援 cbegin()rbegin()
    • 在 C++11 自幹 cbegin() 的方式

14:noexcept

  • C++98:須列出可能拋出的例外,維護十分麻煩。
  • C++11:是否拋出例外比較重要
  • 不拋例外可更加優化
  • 為了例外安全,C++11 容器之 push_back 若 搬移 會拋出例外,則改用 複製。
  • swap 也不要拋出異常 (條件式 noexcept) (例外安全 copy-and-swap 技巧)
  • 屬約定介面,別只為了效能而 noexcept。多數函式為 例外中立:自己不產生,但允許向外傳遞。

15:constexpr

不必再用 template 寫艱深的 meta-programing 啦~

  • 為何在 compile-time 決定數值?
    • 能置於 唯讀記憶體 (尤 嵌入式系統)
    • 可用在 陣列大小、template 參數、enum 值、alignment specifier
    • const 或許是 runtime 才決定
  • constexpr
    • constexpr value: compile-time 就決定的數值
    • constexpr function: 可在 compile-time 計算 constexpr value 的函式
      • 參數皆已在 compile-time 決定 → compile-time 執行、得到 constexpr value
      • 否則 → run-time 執行、普通 value (就只是可以兼用)
  • constexpr function 有所限制
    • C++11:僅可單行 return
    • C++14:literal type 即可
      • void 外的內建型別
      • class with constexpr ctor
  • constexpr class:建構子、成員函式 也可以是 constexpr function

16:內建 mutexatomic 好棒棒!!

  • class Polynomial 為例
    • 「求解」理所當然是 const method
    • 「求解」計算耗時故 cache (mutable 成員)
    • 多執行緒同時「求解」:看似無害,實則 race!
  • 解法
  • (除非打死不用 multi-thread) 務必保證 thread safety

17:更多默默函式

  • 皆為 public 與 inline
    1. default ctor
    2. dtor
      • (C++11) 且為 noexcept
      • base dtor 為 virtual 時才 virtual
    3. copy ctor
    4. copy assign
    5. (C++11) move ctor
    6. (C++11) move assign
  • 生成
    • 皆在需要 (被呼叫卻未宣告) 時才生成
    • ⑴ 在沒有宣告其他 ctor 時生成
    • ⑵~⑹ 的原則是 rule of five (自 rule of three 衍生)
      • 沒有宣告其中任何一者時生成
      • 理由:若有宣告,代表一般規則並不適用。
    • 但為了相容,⑶⑷ 仍保留 C++98 之行為。(並視為 deprecate)
      • 若宣告有 dtor 仍可生成
      • 兩者相互獨立,若宣告了一方,另一方仍可生成。
  • 行為
    • 呼叫 base 與 member 的對應函式
    • 若不支援搬移,則改以複製進行。

Ch4 智慧指標

  • raw pointer 的缺點
    • 是物件還陣列?
    • 是否負責釋放?
    • 如何釋放物件?
    • 如何確保只釋放一次?
    • 是否 dangling?
  • auto_ptr 已由 unique_ptr 取代

18:unique_ptr

  • 單一所有權:只許搬移、不許複製
  • 用於 factory 函式的例子
  • deleter 是型別的一部份,會使物件變大。
  • unique_ptr 雖支援 陣列,但無 operator[] 可用 → 還是用 STL 容器
  • 容易轉型為 shared_ptr

19:shared_ptr

  • Garbage Collection? 程序員的鄙視鏈
  • 原理:reference count → 透過 6 種 特殊成員函式 達成
  • 成本
    • 空間兩倍大 (原始指標 + control block 指標) 💬 能否設計成與原始指標同大? → 也許要再損失效能
    • 須動態配置 control block
    • 須以 atomic 方式控制計數
  • deleter
    • 並非型別的一部份 (相較 unique_ptr) → 方便 置於容器、相互賦值、參數傳遞
    • 放在 control block → 不增加物件本身大小
  • control block
    • 內容
      • reference count
      • weak count → 條款 21
      • deleter
    • 產生時機
      • make_shared
      • unique_ptr 取得
      • 以 原始指標 建立
  • 悲劇:若以 原始指標 建立 shared_ptr 兩次 → 兩份計數、釋放兩次 (未定義行為)
    • 盡量 make_shared
    • 否則 RAII (方可自訂 deleter)
  • 悲劇:this 的場合 → 改用 enable_shared_from_this
    • 💬 須 public 繼承,而由 make_shared 在建立時塞入 control block 訊息。
    • 常以 private ctor 和 factory 函式,來確保物件透過 shared_ptr 建立。
  • 再深入討論成本
    • 可從 unique_ptr 升級,但無法回頭。
  • 不適用於陣列
    • 沒有 operator [] 很不方便
    • 繼承轉型只對單一物件有意義

20:weak_ptr

  • 搭配 shared_ptr 而生
    • .expired() 檢查是否 dangling
    • 不直接使用
      • 呼叫 .lock() 取得 shared_ptr (dangling 則 nullptr)
      • weak_ptr 呼叫 shared_ptr 之 ctor (dangling 則 throw bad_weak_ptr)
  • 使用例 (避免 circular-ref)
    • cache
    • observer

21:make_xxx

(C++14 才有 make_unique)

  • 可以少打幾個字 (重複程式碼)
  • 例外安全 →《Effective C++》條款 17
  • 與 control block 一起配置一次記憶體即可
  • 不該使用的情況
    • 自訂 deleter 時
    • initializer_list 無法完美轉發 → 條款 30 (須先宣告再傳入)
    • 自訂 operator new/delete 時相容性差
    • 在 weak reference 歸零前無法部份釋放
  • 無法 make_shared 時,同時俱備 例外安全 與 效能 的辦法。
    • 別讓其他指令介入其中
    • std::move()

22:pImpl + uniqur_ptr

  • 還記得 pImpl 嗎??
  • 在宣告後而定義前,是「不完全型別」,用途十分有限。
    • 只能宣告其 pointer/reference
    • 定義後才能宣告 instance (如 new/delete)
  • (C++11) 以 unique_ptr 取代 new/delete
    • 以為不必操心 dtor 了嗎? 預設 dtor 為 inline (使用時產生),即實作在「不完全型別」之處。
    • 解法:.h 只有宣告;.cpp 再定義 (可用 = default)。
    • move ctor/assign 亦同
    • copy ctor/assign 則勿忘 deep copy (利用 Impl 自動產生)
  • share_ptr 沒有上述的麻煩;但 unique_ptr (單一所有權) 仍較適當。

Ch5 MOVE

  • 兩者似乎不相干,但皆透過 rvalue reference 實現。
    • 搬移語意 給開發者自行定義 搬移 行為的機會
      • 來避免高成本的 複製 行為
      • 甚至可建立 僅允許搬移 的型別 (如 unique_ptrthread)
    • 完美轉發 讓樣板能接受任意數量的引數,並全數依樣傳送給目標函式。
  • 會有些細微之處不如預期
  • 所有參數皆為 lvalue
    • Widget&& w:rvalue reference 型別的 w,本身也是 lvalue。

23:std::move & std::forward

  • 只是轉型 (沒有 搬移/轉發 任何東西),不產生任何程式碼。
  • std::move 無條件轉型 成為 rvalue (稱 rvalue_cast 也許較貼切)
    • 通常造成搬移
    • 但來源若 const 則無法搬移
      • std::moveconst Foo 成為 const Foo&&
      • 參數 Foo&& f 無法接受 (不可放寬限制) → 無法搬移
      • const Foo& f 可以接受 → 改以複製
  • std::forward 有條件轉型 僅 rvalue 轉為 rvalue
    • 通常用於轉發參數給另個函式
    • 根據額外的 template 參數判斷
  • 技術上 std::forward 可完全取代 std::move,但會看不懂你想幹嘛。

24:請認明 universal reference

  • T&& 幾乎可以參照任何東西 (lvalue、rvalue、…),筆者稱之 universal reference
  • 據初始值 型別推導 達成,如下列兩種情況:
    • template <typename T> void f(T&& x);
    • auto&& x = value;
  • 這些都不是
    • const 的話
    • void f(vector<T>&& x);
    • template <typename T> class { void f(T&& x); };
  • 這些是
    • template <typename... Args> void f(Args&&... args);
    • [](auto&&... params) { … }

25:不可混用

  • rvalue reference 需以 std::move 轉發
    使用 std::forward 麻煩、易錯、又很怪
  • universal reference 需以 std::forward 轉發
    要是 lvaule 被 std::move 搬移了,其值將未定義。
  • 不用 universal reference 不就好了?
    • 須維護兩份函式 (const lvalue reference 與 rvalue reference)
    • w.f("text"); 增加成本
    • 參數個數多 (或 無限) 的排列組合 (2^n)
  • 記得最後才 std::move (否則仍需使用該 rvalue 的話…)
  • 其他使用時機
    • 對回傳的 rvalue reference 使用 std::move
    • 對回傳的 universal reference 使用 std::forward
  • 但 RVO 無需 std::move

26:避免 universal reference + overload

  • 參數型別只要稍微不同 (如 int/short、有無 const),就會被 universal reference 吃掉。
  • 在 ctor 更糟,畢竟自動會有 overload。 (copy/move ctor)

27:條款 26 解法

universal reference + overload 會出事 → 條款 26

  • 放棄 overload
    • 使用不同的函式名稱
    • ctor 不適用
  • 放棄 universal reference (放棄一點效能)
    • 使用 const Type& value
    • 傳值 + std::move → 條款 41
  • 標籤分派 (兩全)
  • std::enable_if
    • 知道怎麼用就好,欲深入探究 → google: SFINAE (Substitution Failure Is Not An Error)
    • !std::is_same<Person, T>::value 型別不是 Person
    • std::decay<T>::value 也不是 Person 的 const 或 reference
    • 要再考慮繼承 → T 若是 Person 的任何衍生類別也都不行
    • 以上排除 ctor 的多載,最後再排除 int 版本
    • C++14 記得用簡單一點的寫法
  • 💬 (C++20) concept/requires
  • 取捨
    • 完美轉發 較有效率,但部份型別無法。
    • 完美轉發 錯誤訊息較難懂 (尤其轉發不只一次時)
    • 只用在效能非常重要之處
    • 彌補難懂錯誤訊息的方法 → static_assert

28:reference collapsing

  • 參考的參考 沒有道理,但在推導 universal reference 時會發生。
  • 解決的規則:(四種組合) 💬 實測
    • 只要有 lvalue 便為 lvalue
    • 兩者皆 rvalue 才為 rvalue
  • std::forward 藉此達成
    ​​template <typename T>
    ​​T&& forward(remove_reference_t<T>& p)
    ​​{
    ​​    return static_cast<T&&>(p);
    ​​}
    
    • lvalue: Type& &&Type&
    • rvalue: Type &&Type&&
  • 四種場合:template, auto, typedef, decltype

29:假設沒有搬移

  • 搬移如何快? 利用改變指標指向這類技巧 (否則並沒有差別)
  • 為了例外安全,若非 noexcept,容器將選擇複製。
  • 寫 template 或是「不穩定」程式碼,就假設沒有搬移。

30:可能轉發失敗的場合

  • { … } 莫名無法推導 → 可以先 auto 宣告再傳入
  • 以 0 或 NULL 作為指標 → 改用 nullptr 啦笨蛋!!
  • 只有宣告的 static const 整數 → 一些龜毛的編譯器會要求定義
  • overload 或 template function
  • bitfield

Ch6 LAMBDA

這些以前就都能做到,只是 lambda 更加易寫易讀。

術語

  • lambda expression:指表示式 (程式碼) 本身
  • closure:執行期的物件
  • closure class:closure 的型別 (每個 lambda 獨有的型別)

31:標明 capture 對象

  • capture
    • by copy
    • by reference
  • closure 生命週期大於該 scope,就要小心 dangling!
    • 可將 capture by reference 改為 by copy
  • 小心 capture by copy 的對象是 member (this 指標)
    • 從 member 複製成 local 再 capture
    • 或 (C++14) init capture → 條款 32
  • global/static 不必 capture 便能使用,別誤解成 capture by copy。
  • 標明 capture 對象就容易發現問題

32:init capture

  • 問題
    • 怎麼 capture by move?
    • capture member 好麻煩 (條款 31)
  • (C++14) init capture [x = y]{…}
  • std::bind 用到再查書 :p

33:auto&& 參數 用 std::forward<decltype(x)>

  • (C++14) lambda 參數也能 auto,猶如 template operator()
  • 代表 lambda 也能有 universal reference (auto&& x)
  • 但是 std::forward<???>??? 要填什麼? (沒 T 可填)
    • 推導一下便知,以 decltype(x) 取代,效果相同。
  • (C++14) lambda 甚至也能接受 不定個數參數 (auto&&...)

34:lambda 賽高

  • 時代演進
  • 好處都有啥?
    • bind 的壞
      • 不易閱讀 (尤如 _1_2)
      • now() 成為 呼叫 std::bind 時間,而非呼叫 函式物件 時間。 → 解法:需雙層 bind
      • 無法接受 overload 函式 → 解法:宣告為函式指標
    • lambda 的好
      • 可 inline
      • 易加上額外行為 → bind 恐須多層
      • capture by reference 較明顯也容易 → bind 需 std::ref()
  • C++11 唯二的不得不 std::bind
    • (C++14) capture by move
    • (C++14) auto 參數

Ch7 平行處理

  • C++11 標準函式庫總算支援 平行處理,保證 多執行緒 程式在不同平台有一致的行為。
  • 先說 future 有兩種 std::futurestd::shared_future,但通常不特意區分。

35:std::async > std::thread

  • 兩種方式平行處理 int work() 函式
    • thread-base: std::thread t(work);
    • task-base: auto f = std::async(work);
  • 背景知識
    • hardware thread: CPU 核心提供 (如 4C8T)
    • software thread: 作業系統提供,以 time sharing 方式在硬體上執行。
    • std::thread: 以物件管理 software thread 之 handle,可能為 null。
  • std::thread 的麻煩
    • 達到 軟體執行緒 上限將拋出 std::system_error (即便 int work() noexcept)
    • 過度訂閱 而 context switch 成本提高
  • std::async 比較好
    • 易取得回傳值 (.get())
    • 可取得所拋出的例外
    • 函式庫可替你實作 thread pool 與 work stealing,解決 過度訂閱。
    • 甚至不產生任何執行緒 → 條款 36 std::launch::deferred
  • 不得不使用 std::thread 的情況很少:⑴ ⑵ ⑶ 略

36:std::lanuch::async

  • std::async 不一定會平行處理
    • std::lanuch::async: 於另個執行緒平行執行
    • std::launch:deferred: 延遲至 getwait 時才執行 (否則不執行)
  • 預設兩者皆可 (高負載時則 deferred),故有以下問題:
    • 不一定會執行
    • 不一定會以相同,或不同執行緒執行。(不適合 thread_local)
    • wait_forwait_until 記得判斷 std::future_status::defered (以 wait_for(0s))
  • 避免預設行為,可自製 real_async

37:std::thread 解構時必須 unjoinable

  • join: 等待執行緒執行完畢
    • joinable: 等待執行、正在執行、執行完了
    • unjoinable: 未 attach、被 move 了、detach 了、join 過了
  • 刻意 std::thread 的範例
    • 10'000'000 增加可讀性
    • 優先權 在啟動前設定較佳 (以暫停狀態初始)
  • std::thread 解構時,卻仍 joinable? → 標準委員會:應終止程式
    • 自動 join 不好:仍須等待結束
    • 自動 detach 更糟:難以預期的背景行為
    • 責任落在自己頭上
  • 自製 thread_raii 決定 join 或 detach
    • 須以 move 方式接受 std::thread
    • std::thread 成員放最後 (最晚開始;最早離開)
    • 提供 .get() 存取底層
    • 確認 joinable 才 .join()
      • 競爭?其他執行緒不應同時操作 std::thread
    • 支援 move
  • 原範例最終選擇 join
    • 最佳解:通知執行緒提早結束 (標準函式庫未提供,須自行實作。)

38:留意 future 的解構行為

  • 兩者概念雷同,但解構行為卻有所不同。
    • std::thread ↔ 底層執行緒 💬 是說 條款 37 吧?
    • std::future ↔ 所指派任務 (未延遲??)
  • callee/producer: std::promise
    • .get_future()
  • caller/consumer: std::future
    • .share()std::shared_future
  • 運算結果放哪?
    • callee? 算完就消失了,不行。
    • caller? 有 shared_future 該歸誰?
    • 因此:兩者之外的 shared state
  • 解構 future 物件
    • 平時:單純清除資料 (計數 -1)
    • 滿足下列全部條件:才會 join (此行為爭辯中)
      • std::async 所建立
      • 策略為 std::lanuch::async
      • 最後的 future (最後的人關燈)
  • 替代方案:建立 packaged_task 並以 std::thread 執行

39:void future

目標 通知其他執行緒此事件發生

方法㈠ 條件變數 (condition variable)

  • 通常搭配 mutex
    • 通知端:.notify_one().notify_all()
    • 接收端
      • 先鎖定 mutex (搭配 std::unique_lock) 💬 以免與其他 接收端 競爭?
      • 再等待:.wait()/.wait_for()/.wait_until()
  • 缺點
    • 不直覺:沒有共享資料需保護,卻使用 mutex。
    • 若通知後才等待,接收端 會卡死。
    • 未處理 偽喚醒 (spurious wakeup)

方法㈡ 布林旗標 (boolean flag)

  • std::atomic<bool> flag(false);
    • 通知端:flag = true;
    • 接收端:while (!flag);
    • 不需 mutex
  • 缺點:輪詢 耗費資源

方法㈢ 混合㈠與㈡

  • 作法
    • 有 mutex 保護,使用 bool 即可。
    • 檢查 flag 來解決 偽喚醒 問題
  • 優劣
    • 通知後再等待也沒問題
    • 方式怪異,不夠乾淨。

方法㈣ promise & future

  • 不傳資料,void 即可:std::promise<void> p;
    • 通知端:p.set_value();
    • 接收端:p.get_future().wait();
  • 優點
    • 不需 mutex
    • 通知後再等待也沒問題
    • 無 偽喚醒 問題
    • 不 輪詢
  • 缺點
    • 動態配置成本
    • 只能設定一次
  • 實務
    • RAII 與 卡死 的問題 (若未能呼叫到 .set_value())
      • 給讀者練習
    • 擴展至多個接收端
      • 基本上就 .get_future().share()
      • 每個執行緒都要自己的 std::shared_future

40:分辨 std::atomicvolatile

  • std::atomic
    • 像有 mutex 保護,但通常以特殊指令實作,更有效率。
      • ++i/--i:讀取-修改-寫入 (read-modify-write, RMW) 可保證 atomic
      • std::cout << i 不保證整個指令都 atomic (僅 讀取 為 atomic)
    • 免於 race condition
    • 確保指令執行順序,免於 編譯器/硬體 在 指令順序 的最佳化。
      • 寫入前 的指令不可出現在 寫入後
    • 不可 copy construct/assign (也因此不可 move construct/assign)
      • 因為硬體通常不支援
      • 須改以 .load().store(),但兩者不合併為 atomic。
  • volatile
    • 與 平行處理 無關 (在部份 語言/編譯器 也許是)
    • 用於 特殊記憶體,免於 重複讀寫 的最佳化。
    • volatile int x; auto y = x; 型別推導捨去 volatile
  • 因為目的不同,兩者可搭配使用。

Ch8 調教

視情況 考慮 使用

41:考慮 call by value

目的 為了效率

  • 總需要複製的參數 如:存放進私有容器

方法㈠ 對 lvalue 複製;對 rvalue 搬移。

  • 缺點:兩份函式
    • 維護麻煩
    • 程式碼膨脹

方法㈡ universal reference

  • 好處:減少了維護上的麻煩
  • 壞處
    • 須實作於 標頭檔
    • 依然 程式碼膨脹
    • 對可轉型成 std::string 的型別皆有作用 → 條款 25
    • 無法以 universal reference 傳遞的情形 → 條款 30

方法㈢ 違反守則或許合理:call by value

  • 並使用 std::move()
    • 複製後的新物件,不影響呼叫端。
    • 最後一次使用,不影響後續行為。
  • 成本不高嗎?
    • C++98:一律 複製建構
    • C++11
      • lvalue:複製
      • rvalue:搬移

成本

方法 場合 caller callee
lvalue 參考 複製
rvalue 參考 搬移
lvalue 參考 複製
rvalue 參考 搬移
lvalue 複製 搬移
rvalue 搬移 搬移
  • 參考 的成本不計
  • universal reference 更能直接 forward 至建構子 (在此不討論)
  • call by value 多了 2 次搬移

討論

  • 標題:考慮 call by value 的時機
    • 搬移成本低
    • 總需要複製
    • 可複製
  • 四個原因
    1. 以效能換其他好處
    2. 無法複製便無 兩份函式 的問題
    3. 搬移成本高便不划算
    4. 不一定複製也不划算
  • 考慮 copy assign 就更複雜了
  • 留意 串連呼叫 累積的成本
  • slice problem

42:emplace

  • 免除暫時物件,直接接受 ctor 參數,emplace 效能通常優於 insert (但不絕對)。
  • 判斷效能的準則
    • ctor 而非 assign
    • 數值與容器元素型別不同
    • 不太會因為重複而遭拒
  • 搭配 shared_ptr 恐資源洩漏 → 可先建立暫時物件再 move 進容器
  • emplace 算是 explicit 呼叫 ctor