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 %}