2020-10-03 # [Effective Modern C++](https://www.tenlong.com.tw/products/9789863478669) ## 導讀 - 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 ```cpp template <typename T> void f(P param); f(expr); ``` 從 `expr` 推導出 `P` 與 `T` - `P` 為 `T&`/`T*` * `expr` 具參考則忽略 * `expr` 含 `const` (而 `P` 本身沒註明的話),`T` 則推導出 `const`。 * 沒啥意外的 - `P` 為 `T&&` * 可想成 lvalue/rvalue 通用的 reference → 條款 24 - `P` 為 `T` (call by value) * `expr` 具參考同樣忽略 * `expr` 含 `const` 也忽略 (`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>`。 * 異於 `auto`,`template <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)` 是 `int`;`decltype((x))` 是 `int&`。 ## 04:求救,怎知推導成啥? - 靠 IDE - 編譯期:`template<typename T> class TypeDetect;` 小技巧 - 執行期:`typeid(x).name()` (不一定可靠) * [Boost TypeIndex](https://www.boost.org/doc/libs/release/doc/html/boost_typeindex.html) # 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` - `0` 是 `int` 不是指標;[`NULL`](https://en.cppreference.com/w/c/types/NULL) 也不是指標。 💬 在 C++,任何指標皆可轉型成 `void*`,但反方向不行,故不將 `NULL` 定義為 `(void*)0`。 - 同時多載 整數 與 指標 造成問題 - `nullptr` 的型別是 [`std::nullptr_t`](https://en.cppreference.com/w/cpp/types/nullptr_t) * 自動轉型成任何 原始指標 * 程式也更加明確 - 例子:樣板函式以 `0` 或 `NULL` 為引數而推導錯誤 ## 09:`using x = y` - 比較好讀一些 (如 function pointer) - 支援 alias template ← 重點 - 之前的慘況 * 須宣告為 `my_struct<T>::type` * 若在 template 中要再加 `typename` - [`<type_traits>`](https://en.cppreference.com/w/cpp/types#Type_traits) * 在 C++14 才改用新式 * 於 C++11 可簡單仿造 ## 10:`enum class` - scoped enum * 一般來說 C++ 遇 `{}` 產生 [scope](http://en.cppreference.com/w/cpp/language/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](http://en.cppreference.com/w/cpp/language/alignas) * 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:內建 `mutex`、`atomic` 好棒棒!! - 以 `class Polynomial` 為例 * 「求解」理所當然是 const method * 「求解」計算耗時故 cache (`mutable` 成員) * 多執行緒同時「求解」:看似無害,實則 race! - 解法 * [`std::mutex`](http://en.cppreference.com/w/cpp/thread/mutex) ([`std::lock_guard`](http://en.cppreference.com/w/cpp/thread/lock_guard)) * [`std::atomic`](http://en.cppreference.com/w/cpp/atomic/atomic) (成本較低,單一變數適用,不可用於多步操作) * 皆為 move only → 使 `Polynomial` 無法複製 - (除非打死不用 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++](/@yipo/SJD5Rgs1b)》條款 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_ptr`、`thread`) * **完美轉發** 讓樣板能接受任意數量的引數,並全數依樣傳送給目標函式。 - 會有些細微之處不如預期 - 所有參數皆為 lvalue * `Widget&& w`:rvalue reference 型別的 `w`,本身也是 lvalue。 ## 23:`std::move` & `std::forward` - 只是轉型 (沒有 搬移/轉發 任何東西),不產生任何程式碼。 - `std::move` 無條件轉型 -- 成為 rvalue (稱 `rvalue_cast` 也許較貼切) * 通常造成搬移 * 但來源若 `const` 則無法搬移 + 經 `std::move` 後 `const 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::is_integral<…>()`](https://en.cppreference.com/w/cpp/types/is_integral) ([`std::remove_reference_t<T>()`](https://en.cppreference.com/w/cpp/types/remove_reference)) * `std::true_type` 和 `std::false_type` * ctor 不適用 - `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`](https://en.cppreference.com/w/cpp/language/constraints) - 取捨 * 完美轉發 較有效率,但部份型別無法。 * 完美轉發 錯誤訊息較難懂 (尤其轉發不只一次時) * 只用在效能非常重要之處 * 彌補難懂錯誤訊息的方法 → `static_assert` ## 28:reference collapsing - 參考的參考 沒有道理,但在推導 universal reference 時會發生。 - 解決的規則:(四種組合) 💬 [實測](https://gist.github.com/yipo/9f445fe8e2b3571d4793006dd54c9dc4) * 只要有 lvalue 便為 lvalue * 兩者皆 rvalue 才為 rvalue - `std::forward` 藉此達成 ```cpp 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 賽高 - 時代演進 * (C++98) [`std::bind1st`/`std::bind2nd`](http://en.cppreference.com/w/cpp/utility/functional/bind12) * (C++11/TR1) [`std::bind`](http://en.cppreference.com/w/cpp/utility/functional/bind) (since 2005) * (C++11/14) [lambda](http://en.cppreference.com/w/cpp/language/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::future` 與 `std::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()`](https://en.cppreference.com/w/cpp/thread/future/get)) * 可取得所拋出的例外 * 函式庫可替你實作 thread pool 與 work stealing,解決 過度訂閱。 * 甚至不產生任何執行緒 → 條款 36 `std::launch::deferred` - 不得不使用 `std::thread` 的情況很少:⑴ ⑵ ⑶ 略 ## 36:`std::lanuch::async` - `std::async` 不一定會平行處理 * **`std::lanuch::async`:** 於另個執行緒平行執行 * **`std::launch:deferred`:** 延遲至 `get` 或 `wait` 時才執行 (否則不執行) - 預設兩者皆可 (高負載時則 deferred),故有以下問題: * 不一定會執行 * 不一定會以相同,或不同執行緒執行。(不適合 `thread_local`) * `wait_for` 或 `wait_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` ↔ 所指派任務 (未延遲??) :::info - 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](https://en.cppreference.com/w/cpp/thread/condition_variable)) - 通常搭配 mutex * 通知端:`.notify_one()` 或 `.notify_all()` * 接收端 + 先鎖定 mutex (搭配 [`std::unique_lock`](https://en.cppreference.com/w/cpp/thread/unique_lock)) 💬 以免與其他 接收端 競爭? + 再等待:`.wait()`/`.wait_for()`/`.wait_until()` - 缺點 * 不直覺:沒有共享資料需保護,卻使用 mutex。 * 若通知後才等待,接收端 會卡死。 * 未處理 *偽喚醒 (spurious wakeup)* + 利用 [第二個參數](https://en.cppreference.com/w/cpp/thread/condition_variable/wait) 傳入條件 + 但 接收端 不曉得條件為何 **方法㈡** 布林旗標 (boolean flag) - <code>[std::atomic<bool>](https://en.cppreference.com/w/cpp/atomic/atomic) flag(false);</code></code> * 通知端:`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` :::info - [ThreadRAII + Thread Suspension = Trouble?](https://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html) * 僅拋出問題與大家討論 - [More on ThreadRAII and Thread Suspension](https://scottmeyers.blogspot.com/2015/04/more-on-threadraii-and-thread-suspension.html) * 若 promise 解構時仍未 `.set_value()`,理當給 future [拋出例外](https://en.cppreference.com/w/cpp/thread/promise/set_exception)。 * 但物件解構順序與建構相反,造成 thread 先卡住,而 promise 卻未能善後。 * 將 thread 宣告於 promise 之前,以對應 future 建構 thread 後,再指定給變數。 + 包裝成類別便於使用 ::: ## 40:分辨 `std::atomic` 與 `volatile` - `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 {%hackmd @yipo/style %}