# Effect Modern C++ Part 2 [Part 1](https://hackmd.io/@Xps7TehyQ1GqEs4toiKu4w/H148LXKzu) # Chapter 5. Rvalue References, Move Semantics, and Perfect Forwarding Move constructor的呼叫時機: 當object被一個rvalue初始化,且該object型態有定義move constructor (或可能被compiler自動產生),move constructor會被呼叫 1. initialization: `T a = std::move(b)`或是`T a(std::move(b))`,其中`b`的型態是`T` 2. function argument passing: `f(std::move(a))`,其中`a`的型態是`T`且`f`參數是`T` (如`void f(T t)`) 3. function return: 在一個回傳值為`T`的function中 (如`T f()`),`return a;`,其中`a`的型態是`T` ## Item 23: Understand std::move and std::forward `std::move`與`std::forward`只是做轉型 `std::move`是無條件將型態轉為rvalue reference,如下 ```cpp= // Example of implementation of move // C++ 11 version templte<typename T> typename remove_reference<T>::type&& move(T&& param) { // use remove_reference to ensure that // '&&' is applied to a type rather than a reference using ReturnType = typename remove_reference<T>::type&&; return static_cast<ReturnType>(param); } // C++ 14 version templte<typename T> decltype(auto) move(T&& param) { // use remove_reference to ensure that // '&&' is applied to a type rather than a reference using ReturnType = remove_reference_t<T>&&; return static_cast<ReturnType>(param); } ``` 使用`std::move`並不代表一定可以move ```cpp= class Annotation { public: Annotation(const std::string text) : value(std::move(text)) // call copy ctor rather than move ctor { ... } private: std::string value; }; ``` 1. 由於`text`是`const std::string`,`std::move(text)`將會轉型成`const std::string&&` 2. `std::string`的move ctor的參數是`std::string&&` 3. 由於不能將`const std::string&&`轉成`std::string&&`,無法使用move ctor 4. 可以將`const std::string&&`轉成`const std::string&`,因此使用copy ctor 5. 因此,若是想要將傳進來的參數做move,則參數型態不要宣告成`const`,如例子中,只要`Annotation(std::string)`就可以 `std::forward`是有條件的將型態轉為rvalue reference,通常用在有universal reference的template function上 ```cpp= void process(const Widget& lvalueArg); // accept lvalue ref void process(Widget&& rvalueArg); // accept rvalue ref template<typename T> void logAndProc(T&& param) { makeLog(...); process(std::forward<T>(param)); } Widget w; logAndProc(w); // call process(const Widget&) logAndProc(std::move(w)); // call process(Widget&&) ``` 1. 當傳入`logAndProc`的是lvalue時,`param`以`Widget&` lvalue初始化,此時`std::forward`不會轉型,因此會呼叫`process(const Widget&)` 2. 當傳入的是rvalue時,`param`用`Widget&&` rvalue初始化,`std::forward`會將之轉型成rvalue,而呼叫`process(Widget&)` 3. 由於`std::forward`多是指用在這種將參數轉發給其他function的時機,此時稱為perfect forwarding 注意若不使用`std::forward`,兩個例子都會呼叫到`process(const Widget&)`,這是因為`param`是個變數,本身是個lvalue,與他的型態無關 另外在`logAndProc`中,不能用`std::move`取代`std::forward`,因為`std::move`會無條件轉型,這樣不管傳入的是lvalue或rvalue,都會呼叫到`process(Widget&&)` ## Item 24: Distinguish universal references from rvalue references 只有在特定的狀況下,`T&&`才會被當成universal reference,其餘狀況皆為rvalue reference 1. 形式一定要是`T&&`,不能包含cv-qualifier 2. `T`一定要參與型態推導 ```cpp= // rvalue ref, No type deduction void f(Widget&&); // rvalue ref, same reason as above Widget&& var1 = Widget(); // universal ref. auto needs type deduction auto&& var2 = var1; // rvalue ref. form is not T&& template<typename T> void g(const T&& param); // rvalue ref. form is not T&& template<typename T> void g(std::vector<T>&& param); // universal ref template<typename T> void h(T&& param); // example of STL vector template<class T, class Allocator = allocator<T>> class vector { public: ... // rvalue ref. // in push_back, T is not involved in deduction // (T is already decided when declaring a vector) void push_back(T&& x); // universal ref. // Args is independent from T // Args must be deduced when emplace_back is used template<class... Args> void emplace_back(Args&&... args); }; ``` universal reference幾乎可以連結到各種物件,const / non-const,violatile / non-violatile,rvalue / lvalue 運作原理在[Item 28](#Item-28-Understand-reference-collapsing) ## Item 25: Use std::move on rvalue references, std::forward on universal references 若function參數是rvalue reference,應使用`std::move` 若function參數是universal reference,應使用`std::forward` 在universal reference上用`std::move`,可能會修改到lvalue ```cpp= struct Widget { template<typename T> void setName(T&& newName) { // can compile, but may modify the lvalue // name = std::move(newName); // correct name = std::forward<T>(newName); } std::string name; }; Widget w; std::string name = "widget"; w.setName(w); // move name into Widget, the content of name is undefined ``` 這個`setName`,可以用兩個function overloading,一個接受`const std::string&`,一個`std::string&&` ```cpp= struct Widget { void setName(const std::string& newName) { name = newName; } void setName(std::string&& newName) { name = std::move(newName); } std::string name; }; ``` 1. Overloading需要維護兩份code 2. 參數限定為`std::string`,若需要接收其他參數,則可能要多寫其他的function 3. 若需要更多參數,universal ref版本可以用parameter pack,使每個參數都是universal ref,overloading版本則無法 4. 可能會比較沒效率 ```cpp= Widget w; w.setName("this is a name"); ``` 若為overloading的版本: a. 用字串陣列建立`std::string`的暫存物件 b. 呼叫rvalue ref版本的`std::string::operator=` 若為universal ref版本(不會有暫存物件產生) a. 字串陣列直接傳入`setName`中 b. 呼叫字串指標版本的`std::string::operator=` 並不是用了rvalue ref,就一定要用`std::move` 用了universal ref,也不是一定就要用`std::forward` 如下例子,僅有最後一行,是將參數轉發,其他地方因為不會修改參數,僅傳入參數名稱 ```cpp= struct Widget { template<typename T> void setName(T&& newName) { // check if the parameter is legal first // function validateName will not change newName if (validateName(newName)) { name = std::forward<T>(newName); } } }; ``` 若function是by value回傳值,且回傳對象是一個rvalue ref或是universal ref的參數時,應使用`std::move`即`std::forward` ```cpp= // rvalue ref Matrix operator+(Matrix&& lhs, const Matrix& rhs) { lhs += rhs; return std::move(lhs); // return lhs; // copy lhs to return } // universal ref template<typename T> Fraction reduceAndCopy(T&& frac) { frac.reduce(); return std::forward<T>(frac); } ``` 1. function以by value回傳 2. 第一個參數為rvalue reference,這在operator中可能很有用,因為可以直接使用lhs來儲存運算結果 3. 若不使用`std::move`回傳,由於lhs是變數,是個lvalue,因此會copy到暫存物件再回傳 4. 因此若`Matrix`有提供move ctor,效率通常會比較好 5. 若`Matrix`沒有move ctor,`std::move`版本,依然會呼叫copy ctor,但若`Matrix`之後提供move ctor,無需改code即可提升效率 6. 同樣情形也可以應用在universal ref和`std::forward`上,回傳時用`std::forward`,才能保證在傳入rvalue時,將rvalue move到回傳值,而用lvalue時使用copy 注意若回傳的不是參數,而是區域變數,不應使用`std::move` ```cpp= // original version Widget makeWidget() { Widget w; ... return w; // we though w will be copied to a tmp object to return } // version we may improve Widget makeWidget() { Widget w; ... return std::move(w); // OK but don't do this } ``` 這是因為compiler通常能為這種case做RVO(Return value optimization) 直接將w建構在callee的回傳值上 能夠進行RVO的條件: 1. 必須是型態與回傳值相同的local variable 2. 回傳的是local variable本身(`return w`),即透過`std::move(w)`回傳,或是透過ref / pointer回傳,都是不行的 在C++ 17之前,RVO不是必須的 但標準有規範,若可以做RVO,但不做此最佳化的時候,必須將回傳值作為rvalue處理(即必須用`std::move`) 也就是說compiler編譯時,要不使用RVO,要不使用`std::move` 因此開發者直接寫`return w`,就可以將問題交給compiler解決 ```cpp= Widget makeWidget() { Widget w; return w; } // If compiler does not apply RVO, the function must be equal as following Widget makeWidget() { Widget w; return std::move(w); } ``` 同樣當有function參數是by value,且型態與回傳值相同時 若是直接回傳該參數,雖然不能做RVO,但在回傳時,compiler也必須將其作為rvalue處理 ```cpp= Widget makeWidget(Widget w) { ... return w; // the same as 'return std::move(w)' } ``` 結論: 1. 當需要做轉發時,應使用`std::move`和`std::forward`將參數轉發 2. 要將rvalue ref或universal ref作為by value的回傳值時,也應使用`std::move`和`std::forward` 3. 但如果是要將local variable以by value形式回傳時 (return type與該變數型態相同),不應使用`std::move` ## Item 26: Avoid overloading on universal references ```cpp= std::multiset<std::string> names; void logAndAdd(const std::string& name) { log("logAndAdd"); names.emplace(name); // add name. // emplace uses universal ref } std::string s1("s1"); logAndAdd(s1); // pass lvalue logAndAdd(std::string("s2")); // pass rvalue logAndAdd("s3"); // pass string literal ``` 1. `s1`是lvalue,在pass進`logAndAdd`時,`name`是連結到lvalue,在存入`names`時,無法避免copy 2. `s2`是個rvalue,在傳入`logAndAdd`時,`name`是連結到一個rvalue,由於`name`本身是lvalue,因此存入`names`時,依然要copy。但其實因為連結到rvalue,其實是可以move 3. `s3`是string literal,傳入`logAndAdd`時,會先建立暫存物件,`name`一樣是lvalue,因此會copy進`names`,此狀況其實應該可以直接把`s3`pass給`names`,連暫存物件都不需要建立 分析上述三點,應該用universal ref來改善 ```cpp= std::multiset<std::string> names; template<typename T> void logAndAdd(T&& name) { log("logAndAdd"); names.emplace(std::forward<T>(name)); } ``` 1. pass`s1`時,和原本狀況一樣,`name`會連結到lvalue,因此`names`會收到lvalue 2. pass`s2`時,`name`連結到rvalue ref,`names.emplace`可以做move 3. pass`s3`時,`name`會是string literal,`names`可以直接從string literal建立物件 有了univeral ref後,若還有其他overloading function,可能會有問題 ```cpp= void logAndAdd(int index) { ... } std::string s1("s1"); logAndAdd(s1); // call universal ref version logAndAdd(std::string("s2")); // call universal ref version logAndAdd("s3"); // call universal ref version logAndAdd(0); // call int version short index = 0; logAndAdd(index); // call universal ref version ``` 1. 若是用`int`呼叫`logAndAdd`,會呼叫到`int`版本,這是因為`logAndAdd(int)`是exactly match 2. 若用`short`呼叫,有兩個版本選擇,一個是template產生的short版本,一個是`int`版本,由於template是exactly match,而`int`版本要經過promotion才能符合,因此會選擇universal ref 若universal ref function是constructor,可能會更糟 ```cpp= class Person { public: template<typename T> Person(T&& n) : name(std::forward<T>(n)) { ... } // may be customized or compiler generated Person(const Person& p) { ... } // may be customized or compiler generated Person(Person&& p) { ... } private: std::string name; }; Person p1("a person"); auto p2(p1); // call universal ref version const Person p3("another persion"); auto p4(p3); // call copy ctor ``` 1. `Person`定義一個universal ref版本的ctor,copy和move ctor可能是使用自定義,或是compiler自動產生的 2. 當用`p1`去建立`Person`物件`p2`時,有兩個選擇 一個是universal ref產生的,另一個是copy ctor 由於copy ctor需要的是`const Person&`,`p1`需要增加`const`qualifier才能match,因此會呼叫universal ref 3. 用`p3`建立`Person`物件`p4`時,也有兩種選擇,universal ref和copy ctor同樣好的狀況下,compiler會挑選non-template版本,因此呼叫copy ctor 若有繼承時,可能會更加困惑 ```cpp= class SpecialPerson : public Person { public: SpecialPerson(const SpecialPerson& rhs) : Person(rhs) // will call Person(T&&) version { } SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) // will call Person(T&&) version { } }; ``` 在copy / move ctor中,pass給`Person`class的型態實際上是`SpecialPerson`,因此universal ref版本會將型態推導為`SpecialPerson`,優於`Person`的copy / move ctor ## Item 27: Familiarize yourself with alternatives to overloading on universal references ### 避免overloading 利用function名稱將universal ref版本和其他版本分開 不適用於constructor ### 用const T& 代替 universal ref 即以前C++ 98的方式,優點是簡單易懂,缺點是效能可能較低 ### 用 T (傳值) 取代 universal ref 若確定function參數就是要copy / move 給其他參數 (如constructor),用by value的方式可能可以同時兼顧效能與簡單的設計 [Item41](#Item-41-Consider-pass-by-value-for-copyable-parameters-that-are-cheap-to-move-and-always-copied)專門描述這樣的狀況 ```cpp= class Person { public: Person(std::string n) : name(std::move(n)) { ... } Person(int index) { ... } } ``` ### 用tag dispatch 雖然絕大多數的參數都能用universal ref去做match,但如果function參數中有non universal ref,只要該參數不符合,就能夠呼叫其他的function,這樣的non universal ref參數稱為tag,能夠引導compiler呼叫不同的overloading function ```cpp= template<typename T> void logAndAdd(T&& name) { logAndAddImpl( std::forward<T>(name), // std::is_integral<T>() // wrong. T may be a lvalue ref std::is_integral<typename std::remove_reference<T>::type>() ); } // universal ref version template<typename T> void logAndAddImpl(T&& name, std::false_type) { names.emplace(std::forward<T>(name)); } // int version void logAndAddImpl(int index, std::true_type) { ... } ``` 1. 兩個`logAndAddImpl`function即是[Item26](#Item-26-Avoid-overloading-on-universal-references)中本來的`logAndAdd`實作 2. `logAndAddImpl`的最後一個參數即為標籤,型態分別是`std::false_type`和`std::true_type`,不論第一個參數是什麼,如果最後一個參數型態是`std::true_type`,就呼叫`int`版本的`logAndAddImpl`,反之呼叫universal ref版本 3. 新的`logAndAdd`只接受universal ref,是給user的interface 4. 在呼叫`logAndAddImpl`時,最後一個參數用`std::is_integral`去判斷`T`是否是整數型態,如果是整數,`std::is_integral<T>`會是`std::true_type`,藉此引導compiler呼叫`int`版本 5. `std::is_intergral`中應使用`std::remove_reference`,確定傳給`is_integral`的不是一個reference NOTE: 當傳lvalue給`logAndAdd`時 (e.g. 傳入變數`int i`),`T&&`和`T`都推導為`int&` T為reference to int而不是int,因此`std::is_integral<T>`為false 當傳入rvalue時(e.g. `std::move(i)`),`T&&`為`int&&`,`T`為`int` 為確保兩種都可以work,才要用`remove_reference` 6. `std::true_type`/`false_type`時常用來做型態類型的`true`/`false`,在template code中時常被使用 此方式也不適用於constructor,因為tag dispatch的基礎是interface只有一個,且用universal ref表示 compiler可能會自行產生copy / move constructor,打破這個規則 ### 限制接受universal ref參數的template 可以用`std::enable_if`來限制universal ref只能接受特定種類的參數 `std::enable_if`利用[Substitution Failure Is Not An Error](#SFINAE) (SFINAE),讓template變成無法被產生,compiler就無法挑選該template 概念上,預設template都是enable狀態,都是compiler選擇的對象。`std::enable_if`使得template僅有在條件符合的狀況下才是enable,不符合就是disable ```cpp= class Person { public: template<typename T, typename = typename std::enable_if<condition>::type> Person(T&& n); }; ``` 1. constructor的第二個template參數即為enable的條件,若條件不符 2. 第二個參數的`typename =`沒有名稱,是因為這參數只代表條件,實際上程式碼不會去使用 3. 第二參數的`typename std::enable_if`中,`typename`是必須的,因為`enable_if`中會用到`T`,此`typename`標記了`T`是一個template parameter 4. `condition`為條件 5. `std::enable_if<condition>::type`的`type`實際是什麼並不重要。重要的是如果條件符合,`type`才會產生,如果條件不符,`std::enable_if`不會有`type`,而依據SFINAE,此template推導會失敗(因為`enable_if`沒有`type`),導致此template function不會出現 利用`enable_if`,我們可以讓universal ref的constructor只在以下條件時產生 1. 當`T`不是Person時 2. 當`T`不是Person的衍生type時 3. 當`T`不是整數型態時 ```cpp= class Person { public: template< typename T, typename = typename std::enable_if< !std::is_base_of<Person, typename std::decay<T>::type>::value && !std::is_integral<std::remove_reference_t<T>>::value >::type explicit Person(T&& n); explicit Person(int index) { ... } }; ``` 1. `enable_if`中包含兩個條件,用`&&`連接 2. 第一條件表示`T`的base type不可以是`Person`,這也包含`Person`本身 3. `std::decay`是讓`T`退化,此包含移除reference和cv-qualifier,若`T`是function或陣列型態,也會退化成pointer,這功能確保`T`僅代表型態 (若包含reference等會導致`is_base_of`的結果不如我們預期) 4. `is_integral`的條件和上一段的[tag dispatch](#用tag-dispatch)相同 5. 在C++ 14中可用`enable_if_t`取代`enable_if<>::type`,減少程式碼,`std::decay_t`也是一樣的方式,此時`typename`就可以省略了 ```cpp= class Person { public: template< typename T, typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference_t<T>>::value > explicit Person(T&& n); explicit Person(int index) { ... } }; ``` 結論: 通常用universal ref來做perfect forward可以增進效率,其允許string literal直接傳入function之中,減少暫存物件產生的成本 但如果傳入不合法的參數時,compiler所產生的error message可能會很多,也很難理解 如 ```cpp= Person p(u"a char16_t string"); // use const char16_t[] to construct ``` `std::string`和`int`無法用`char16_t`的array/pointer產生,因此這是不合法的參數 如果不用universal ref,error message會直接在這行指出`const char_16_t[]`是不合法參數 但用universal ref,參數會傳入constructor,直到perfect forward給`Person`的data member時,才會發生錯誤,error message會顯示在constructor中,訊息也很長,比較不直覺 此問題在多次perfect forward時候可能會更糟糕 對`Person`的例子,因為我們清楚知道universal ref的參數是用來初始化`std::string`,可以透過在constructor中加入`static_assert`來提供更好的error message ```cpp= class Person { template<typename T, typename = std::enable_if_t<...> Person(T&& n) { static_assert( std::is_constructible<std::string, T>::value, "Parameter n can't be used to construct a std::string" ); ... } }; ``` ### SFINAE TODO ## Item 28: Understand reference collapsing universal ref會根據傳入的參數不同,導致`T`的不同,有可能將`T`推導為lvalue ref,或是一個rvalue ```cpp= template<typename T> void func(T&& param); Widget widgetFactory(); // return rvalue func(widgetFactory()); // T is deduced to Widget Widget w; func(w); // T is deduced to Widget& ``` 請注意C++不允許宣告reference to reference,如下code是不合法的 ```cpp= int a = 0; int& r = a; // ok int& & rr = r; // invalid ``` 但當把lvalue傳入universal ref時,`T`被推導為lvalue ref,再將之與`&&`結合會產生`T& &&` compiler不會把這樣的狀況作為error,是因為有reference collapse的規則,把這種ref to ref的狀況變成合法的reference reference collapse只會發生在4種狀況 1. template實體化時 2. `auto`的推導 3. 建立與使用`typedef`和alias declaration 4. `decltype` 由於reference有分兩種:lvalue ref和rvalue ref 因此這種reference to reference的形式有四種 1. `T& &` 2. `T& &&` 3. `T&& &` 4. `T&& &&` reference collapse的規則是:只要其中一個reference是lvalue ref,結果就會是lvalue ref,僅有當兩者皆為rvalue refer時,結果才會是rvalue ref 1. `T& &` -> collapse to `T&` 2. `T& &&` -> collapse to `T&` 3. `T&& &` -> collapse to `T&` 4. `T&& &&` -> collapse to `T&&` ```cpp= template<typename T> void func(T&& param); Widget w; func(w); // apply Widget to func void func(Widget& && param); // reference collapse will apply to the above line // and will become void func(Widget& param); ``` 這機制是`std::forward`運作的核心,如以下的範例 ```cpp= // example of std::forward implementation template<typename T> T&& forward(typename remove_reference<T>::type& param) { return static_cast<T&&>(param); } template<typename T> void f(T&& fParam) { someFunc(std::forward<T>(fParam)); } ``` 當`f()`傳入Widget的lvalue時,`T`被推導為`Widget&`,以下將型態帶入`f()`和`std::forward`的結果 ```cpp= // Apply Widget& to T Widget& && forward(typename remove_reference<Widget&>::type& param) { return static_cast<Widget& &&>(param); } void f(Widget& fParam) { someFunc(std::forward<Widget&>(fParam)); } ``` 將reference collapse apply到`std::forward`就會如下 ```cpp= // Apply Widget& to T Widget& forward(Widget& param) { return static_cast<Widget&>(param); } ``` 因此得到結果,會將`param`轉型為`Widget&`,並且回傳`Widget&` (即不做事情),這符合`std::forward`對lvalue參數的行為 當`f()`傳入Widget的rvalue時,`T`推導為`Widget`,以下是帶入`Widget`的結果 ```cpp= // Apply Widget to T Widget&& forward(typename remove_reference<Widget>::type& param) { return static_cast<Widget&&>(param); } void f(Widget&& fParam) { someFunc(std::forward<Widget&&>(fParam)); } ``` 這裡因為沒有reference to reference的情況,不會發生reference collapse 而`std::forward`會把`param`轉型為`Widget&&`並回傳`Widget&&` `typedef`、alias declaration與`decltype`也會apply reference collapse ```cpp= int i = 0; int& r = i; decltype(r)&& rvalue_ref = r; // type of rvalue_ref is int& using RvalueRef = int&&; RvalueRef& rvalue_ref2 = i; // type of rvalue_ref2 is int& ``` ## Item 29: Assume that move operations are not present, not cheap, and not used 此條款描述move operation在某些狀況下可能不如預期 1. Move operation根本不存在 從[Item 17](#Item-17-Understand-special-member-function-generation)可知道,當有customized dtor、copy ctor、copy assignment或是任一一個move operation時,剩下的move operation不會被產生 因此許多狀況下,除非開發者有特別定義move操作,否則class可能並不支援move語意 2. Move operation成本不見得比較低 即使有明確提供Move operation,這成本也不一定比copy低 Move操作成本比較低,通常都是因為記憶體被配置在heap上(如`std::vector`),做Move時是將指標做轉移,而不是像copy一樣配置一塊新的記憶體 ![](https://i.imgur.com/Hzc43hL.png) 但若記憶體都是在stack上(如`std::array`),做move時,可以對每個data member都使用move操作,但假設data member不支援,那實際上成本也是和copy一樣 如下圖,當`aw1`要被move到`aw2`時,`std::array`會對每個element做move操作,如果`Widget`有支援move,那整體速度就可能會比copy快 但若`Widget`不支援move操作,則實際上還是會copy ![](https://i.imgur.com/q6VXJMf.png) 3. Move operation不見得會被使用 這類型和實作有關 a. 如`std::string`可能會使用small string optimization (SSO),當字串長度小於一定值(如15個char)時,`std::string`會使用物件內部的buffer來存,而不在heap上配置空間 對這種`std::string`做move操作,實際上也會copy。當然通常這樣的成本都不會太高 b. 另一種可能是class並不符合呼叫move operation的條件,如[Item 14](#Item-14-Declare-functions-noexcept-if-they-won’t-emit-exceptions)提到,STL中某些容器操作有提供例外安全的保證,對此種容器而言只有當element有提供`noexcept`的move operation,才會使用,否則會copy 4. Move的來源是lvalue 通常狀況下對lvalue做move operation,實際上還是會copy 只有在如[Item 25](#Item-25-Use-std::move-on-rvalue-references-std::forward-on-universal-references)最後所提到的,一個function以by value方式回傳一個local variable時,才會對lvalue做move operation而不是copy 在一些無法確定type的情況(如template),應要有心理準備,假設處理的class中,move可能不存在、成本不低、且可能不被使用。這狀況下就不應依賴move operation所帶來的好處,而要保守的處理class ## Item 30: Familiarize yourself with perfect forwarding failure cases forward: 轉發表示一個function將參數forward給另一個function,令接收的function可以接收到相同的參數,通常討論轉發時,只討論reference (因為by value時候僅是copy) perfect forward: 也是轉發參數,但不僅是轉發物件,同時也將參數的type也轉發給下一個function,這包含該reference是lvalue或是rvalue,以及是否有cv-qualifier 以下是通常用到perfect forward的function會有的樣子,參數是variadic的universal ref body則是用`std::forward`把`params`傳給其他function ```cpp= template<typename... Ts> void fwd(Ts&&... params) { f(std::forward<Ts>(params)...); } ``` perfect forward失敗的意思是,將參數直接傳遞給`f`時的狀況,和將參數傳給`fwd`,再傳給`f`時的狀況不同 通常是兩種會導致失敗 1. compiler無法推導出`fwd`中某些參數的type,以至於無法編譯 2. compiler從`fwd`推導出錯誤的type,以至於將錯誤的type傳遞給`f` 以下是perfect foward失敗的情境 ### 大括號初始子 由於template parameter不能接受大括號初始子(除了`param`明確寫為`std::initializer_list<T>`) 因此以下狀況下,直接呼叫`f`可以用大括號,但透過`fwd`就不行 ```cpp= void f(const std::vector<int>& v); f({1, 2, 3}); // ok fwd({1, 2, 3}); // error. Fail to deduce type T // alternative way auto v = { 1, 2, 3}; // ok. v is std::initializer_list fwd(v); // ok. perfect forward to f ``` ### 用`0`或`NULL`作為null pointer 用`0`或`NULL`傳給`fwd`時,type會被推導為`int`而不是指標 此狀況用`nullptr`即可 ### 只有宣告的的`static const`整數data member 通常,對於`static const`的data member,如果只是要取值,是只需要宣告不需要定義 這是因為compiler可以對這些數值做const propagation的最佳化 ```cpp= class Widget { public: static const int Value = 28; }; // const int Widget::Value = 28; // No definition here std::vector<int> v; v.reserve(Widget::Value); // OK. does not use the Widget::Value's address void g(const int&); // g(Widget::Value); // error. Invoke g will use Widget::Value's address ``` 同理,如果將`Widget::Value`傳入`fwd`,也會導致編譯(linkage)錯誤,因為universal ref是一個reference,會對`Widget::Value`取值 ```cpp= f(Widget::Value); // OK. fwd(Widget::Value); // linkage error. Use Widget::Value's address ``` ### 傳遞一個有overloading的function或傳遞template function 假設function `f`是接受一個function pointer (or function type)如下 ```cpp= void f(int (*pf)(int)); // the following declaration is the same as above void f(int pf(int)); // processVal is overloading int processVal(int); int processVal(int, int) f(processVal); // ok. pass processVal(int) // fwd(processVal); // error. Don't know which processVal should use ``` 將`processVal`傳給`f`可以work,是因為compiler知道`f`需要的是哪種type 但在`fwd`時,光從universal ref上,無法判斷應該要哪個`processVal`,因此有error 同樣的問題在template function也是一樣,因為template function本身代表functions的集合,而不是單一一個function ```cpp= tempalte<typename T> T workOnVal(T param); // fwd(workOnVal); // error. ``` 要解決此問題,就要在傳給`fwd`時,明確認compiler知道傳遞的型態是什麼 ```cpp= using ProcessFunc = int(*)(int); ProcessFunc proc = processVal; // ok. processVal(int) is chosen fwd(proc); // ok. fwd(static_cast<ProcessFunc>(processVal)); // OK fwd(static_cast<ProcessFunc>(worokOnVal)); // OK ``` ### 傳遞bitfield C++標準規定,universal ref不能接受non-const的bitfield ```cpp= struct A { std::uint32_t field1:16, field2:16; }; void f(std::size_t sz); A a; f(a.field2); // ok fwd(a.field2); // error. non-const ref cannot bind to bitfield ``` 由於任何傳遞bitfield的狀況,compiler都會將之變成傳遞copy of the bitfield value 這是因為bitfield沒有辦法取address,無法被reference 而當bitfield是`const`時可以被傳遞,則是因為`const`時不能修改bitfield,compiler可以安心的把bitfield的值取出來,copy到function的參數上 ```cpp= const A a; f(a.field2); fwd(a.field2); // ok a.field2 is const ``` 因此要解決此問題,只要先將bitfield的值copy到一個變數,再傳遞該變數就可以了 ```cpp= auto field = static_cast<std::uint16_t>(a.field2); fwd(field); ``` # Chapter 6. Lambda Expressions Lambda是C++ 11後引入的語法糖,並在C++ 14得到擴充 語法為 ```cpp= [ capture list ] (parameters ) -> trailing-return-type { method definition } ``` 若`trailing-return-type`可以被compiler推導,也可以不寫 當寫一個lambda時,compiler實作會給他一個獨一無二的type 如`auto f = []() {};`,`f`是一個object,有ctor和dtor,並且有`operator()()` ```cpp= auto f = []() { std::cout << "f()" << std::endl; }; // the same as struct anonymous { inline auto operator()() const { std::cout << "f()" << std::endl; } }; int i; auto f2 = [&i]() { std::cout << i << std::endl; }; // the same as struct anonymous2 { int& m_i; anonymous2(int& i): m_i(i) {} inline auto operator()() const { std::cout << i << std::endl; } }; auto f3 = [i]() { std::cout << i << std::endl; }; // the same as struct anonymous3 { int m_i; anonymous3(int i): m_i(i) {} inline auto operator()() const { std::cout << i << std::endl; } }; ``` 1. lambda expression是一個expression,他是程式碼的一部份,如`auto f = []() {...}`中,`[]() {...}`是lambda expression 2. closure是lambda在runtime時建立的object,依據capture mode不同,closure擁有資料的copy或是reference,如`auto f = []() {...}`中,`f`是runtime所建立的closure 3. closure的本體是一個callable的物件,如上例中的`anonymous`、`anonymous2`和`anonymous3`所產生的物件 4. closure class是實體化closure的類別,如例子中的`struct annonymous` 5. capture方式有by value,by ref和no capture,以下是capture方式 | 寫法 | Capture方式 | | ------------ | ------------------------------------------------- | | `[](){}` | no capture | | `[=](){}` | captures everything by copy | | `[&](){}` | captures everything by reference | | `[x](){}` | captures x by copy | | `[&x](){}` | captures x by reference | | `[&,x](){}` | captures x by copy, everything else by reference | | `[=,&x](){}` | captures x by reference, everything else by value | 5. 小括號表示要傳入的參數 6. 如果沒有capture(如`anonymous`),class裡就沒有data member,但closure還是一個object,能否轉型成function pointer要看compiler實作而定 7. 如果有capture(如`anonymous2`和`anonymous3`),則有data member,當by value capture時,就會copy進data member 若要把lambda作為參數傳入function時,可以利用template完成 ```cpp= template<typename Func> void f(Func func) { func(); } auto lambda_func = []() { ... }; f(lambda_func); ``` 當capture mode有by value時,`operator()`by default都是const function,如果要讓其不是const,可以加入`mutable`specifier ```cpp= int i = 0; int j = 0; auto f = [i]() mutable { i++; }; // mutable must exist auto f2 = [i, &j] mutable { i++; }; // mutable must exist ``` 若在member function中,想要capture member data的話,需要capture `this`指標,使用`[this]`、`[=]`或`[&]`都可以 需要注意capture的是`this`而不是member data,因此member data是可以被修改的 ```cpp= class A { public: int m_i; void f() { // the followings are the same [=]() { std::cout << m_i << std::endl; m_i++; }(); [this]() { std::cout << m_i << std::endl; m_i++; }(); [&]() { std::cout << m_i << std::endl; m_i++; }(); } }; ``` 可以用lambda建立high-order的function ```cpp= auto less_than = [](int x) { return [x](int y) { return y < x; }; }; auto less_than_5 = less_than(5); std::cout << less_than_5(3) << std::endl; // print 1 std::cout << less_than_5(10) << std::endl; // print 0 ``` ### C++ 14以後加入的功能: 可以在capture mode中定義變數,如下例中 `tmp`是by value的capture `i`,`tmp2`是by ref的capture `i`,`tmp3`是by value的被初始化為`0` ```cpp= int i = 10; auto f = [tmp = i, &tmp2 = i, tmp3 = 0]() { std::cout << tmp << "," << tmp2 << "," << tmp3 << std::endl; }; ``` Lambda的參數可以用`auto`,讓compiler做型態推導 ```cpp= auto f = [](auto a, auto b) {}; // the same as struct anonymous { tempalte<typename T0, typename T1> auto operator()(T0 a, T1 b) const { } }; ``` Lambda也可以接受variadic generic參數,等同於variadic template ```cpp= auto f = [](auto... param) {}; // the same as struct anonymous { template<typename T> auto operator()(T... param) { } }; ``` 接受generic type參數的lambda適用template型態推導,因此也可以用cv-qualifer、universal ref 而由於template不接受大括號初始化,因此generic type lambda也無法接受大括號初始子 ```cpp= auto f = [](auto&& a) {}; // ok. universal ref auto g = [](auto a) {}; // g({1, 2, 3}); // error. cannot infer type for {1, 2, 3} ``` ## Item 31: Avoid default capture modes 當closure的生命週期大於capture的參數時,by reference capture mode會導致dangling reference ```cpp= using FilterContainer = std::vector<std::function<bool(int)>; FilterContainer filters; void addDivisorFilter() { int cal1 = computeValue1(); int cal2 = computeValue2(); int divisor = computeDivisor(cal1, cal2); // dangling ref to divisor filters.emplace_back( [&](int value) { return value % divisor == 0; } ); // explicitly specify referencing to divisor // still dangling ref to divisor filters.emplace_back( [&divisor](int value) { return value % divisor == 0; } ); } ``` 1. 在`addDivisorFilter`中,lambda capture `divisor` by reference,並且被塞入全域變數中 2. 但`divisor`在離開`addDivisorFilter`後就被銷毀,導致`filters`之後使用closure時,會ref到不存在的東西 3. 明確的寫`[&divisor]`可以提醒開發者,lambda依賴於`divisor`,`divisor`的生命週期應該至少要和該closure一樣長 4. 如果確定closure會立刻被使用 (如STL演算法)且不會被複製,by ref是可以的 ```cpp= template<typename T> void work(const std::vector<T>& container) { auto cal1 = computeValue1(); auto cal2 = computeValue2(); auto divisor = computeDivisor(cal1, cal2); if (std::all_of(begin(container), end(container), [&](const T& value) { return value % divisor == 0; })) { ... } } ``` `addDivisorFilter`的問題,可以用capture by value方式解決 但某些狀況,如果capture的是`this` pointer,依然會有dangling pointer的問題 ```cpp= class Widget { public: void addFilter() const; private: int divisor; }; void Widget::addFilter() const { // capture this pointer by value filters.emplace_back( [=](int value) { return value % divisor == 0;} ); } void func() { auto p = std::make_unique<Widget>(); p->addFilter(); } // p is deleted when leaving the scope // filters holdes a closure with dangling pointer to Widget ``` 1. lambda的capture只能對區域變數、參數、以及`this`指標做擷取,因此如下的寫法是錯的 ```cpp= void Widget::addFilter() const { // error. divisor not available filters.emplace_back( [](int value) { return value % divisor == 0; } ); // error. no local divisor to capture filter.emplace_back( [divisor](int value) { return value % divisor == 0; } ); } ``` 2. 在member function中,使用by value的預設capture mode時,`this`會以by value方式被擷取 ```cpp= void Widget::addFilter() const { filter.emplace_back( [=](int value) { ... } ); // the same as following auto currentObjPtr = this; filter.emplace_back( [currentObjPtr](int value) { ... } ); } ``` 3. 在by value狀況下想解決生命週期問題,可以明確地將member data `divisor`先copy至區域變數,在做capture ```cpp= void Widget::addFilter() const { // C++ 11 version auto divisorCopy = divisor; filter.emplace_back( [divisorCopy](int value) { value % divisorCopy == 0; } ); } ``` 4. C++ 14更可以用init capture (generalized lambda capture)方式解決 ```cpp= void Widget::addFilter() const { // C++ 14 version filters.emplace_back( [divisor = divisor](int value) { value % divisor == 0; } ); } ``` 預設的by value capture mode還有一個缺點,會讓開發者以為該lambda可以自給自足,不與其他物件有關聯 但當lambda碰到`static`變數時,lambda無法擷取`static`變數,但卻能使用,而使用預設的by value capture mode會讓人以為`static`變數的值被copy進closure裡了,實際上不然 ```cpp= void addDivisorFilter() { static auto cal1 = computeValue1(); static auto cal2 = computeValue2(); static auto divisor = computeDivisor(cal1, cal2); // does not capture anything filters.emplace_back( [=](int value) { return value % divisor == 0; } ); ++divisor; // modify divisor value } ``` 上例中,lambda沒有capture任何東西,該lambda直接使用`static`變數`divisor`,當`divisor`值改變時,lambda執行的內容就會改變 但若閱讀程式碼時粗心,看到`[=]`就誤以為lambda已將所有需要的物件都copy至closure了,可以自給自足,造成誤會 ## Item 32: Use init capture to move objects into closures lambda無法使用move capture,在C++ 11和C++ 14分別有不同的解法 C++ 14可以用init capture做到 init capture必須有 1. lambda產生的closure class中,所使用的data member名稱 2. 用來初始化該data member的expression 3. init capture不支援default capture mode ```cpp= class Widget { public: bool isValidated() const; }; auto p = std::make_unique<Widget>(); auto func = [p = std::move(p)] { return p->isValidated(); }; // the same as above auto func2 = [p = std::make_unique<Widget>()] { return p->isValidated(); }; ``` 1. `std::unique_ptr`只能move,不能copy 2. 在`[p = std::move(p)]`中,左邊的`p`代表closure class中data member名稱,scope是在closure class中,和`func`所在的scope不同 3. 右邊的`std::move(p)`代表用來初始化data member的expression,scope和`func`相同,且和closure class的scope不同 4. 初始化列表中所代表的意思是:在closure中建立data member `p`,並且將`std::move`套用在區域變數`p`上,其結果用來初始化data member `p` 5. lambda的body的scope和closure class相同,因此裡面使用的`p->isValidated()`是使用closure class中的data member `p` 6. `func2`的`p`是直接用expression `std::make_unique`初始化 由於lambda只是將建立callable的class做簡化,在C++ 11中,可以透過自定義class達到相同效果 ```cpp= class IsValidated { public: using DataType = std::unique_ptr<Widget>; explicit IsValidated(DataType&& p) : p(std::move(p)) { } bool operator()() const { return p->isValidated(); } private: DataType p; }; auto func = IsValidated(std::make_unique<Widget>()); ``` C++ 11中也可以用lambda做模擬 1. 將要capture的物件用`std:move`搬移到`std::bind`的function object中 2. 在lambda的參數中,使用reference,ref到captured的物件 ```cpp= auto p = std::make_unique<Widget>(); auto func = std::bind( [](const std::unique_ptr<Widget>& p) { p->isValidated(); }, std::move(p) ); ``` 1. `std::bind`與lambda一樣,會回傳一個callable的object,以下代稱bind object 2. `std::bind`的第一個參數是一個callable object,第二參數表示要傳給callable object的參數 3. bind object將所有傳給`std::bind`的參數都保存下來,成為data member,若參數用lvalue方式傳給`std::bind`,用copy,若用rvalue傳遞,用move 4. 呼叫bind object時,bind object會將data member當作參數,callable object (當初`std::bind`的第一個參數) 5. 此版本與C++ 14的不同是,再呼叫`func`時,使用的`p`是bind object內部的data member的lvalue reference,而不是rvalue reference 由於lambda預設產生的function是`const`,`std::bind`中,要用`const T&`來宣告參數 如果要能夠修改參數的值,要使用`mutable`標記lambda ```cpp= auto func = [p = std::make_unique<Widget>()] mutable { ... // p is mutable }; auto func2 = std::bind( [](std::unique_ptr<Widget>& p) mutable { // p is mutable }, std::make_unique<Widget>() ); ``` ## Item 33: Use decltype on auto&& parameters to std::forward them C++ 14導入generic labmda,讓參數可以透過template推導型態 ```cpp= auto f = [](auto x) { return func(x); }; // the same as struct anonymous { template<typename T> auto operator()(T x) const { return func(x); } }; ``` 此lambda僅是將`x` forward給`func`,如果`func`對lvalue和rvalue處理方式不同,則應使用universal ref,並用`std::forward` ```cpp= auto f = [](auto&& x) { return func(std::forward<decltype(x)>(x)); }; ``` 1. `std::forward`需要template parameter,但在lambda中,沒有明寫template parameter,因此需要用其他方式來決定 2. `decltype`可用來取得`x`的型態,因此`decltype(x)`可以傳給`std::forward`來做perfect forward NOTE: 在一般template function用`std::forward`時,通常是以下狀況 ```cpp= template<typename T> void fwd(T&& param) { func(std::forward<T>(param)); } auto f = [](auto&& x) { return func(std::forward<decltype(x)>(x)); }; int x; fwd(x); f(x); fwd(0); f(0); ``` 1. 當傳入lvalue (`x`)時,`T`是lvalue reference,給`std::forward`的`T`是`int&`,在lambda中傳入lvalue時,`decltype(x)`也是lvalue ref,兩者狀況一致 ```cpp= // Apply int& to template parameter T (when calling fwd(x)) void fwd(int& && param) { func(std::forward<int& &&>(param)); } // after ref collapse void fwd(int& param) { func(std::forward<int&>(param)); } // Apply int& to auto (when calling f(x)) auto f = [](int& && x) { return func(std::forward<decltype(x)>(x)); }; // after ref collapse auto f = [](int& x) { return func(std::forward<int&>(x)); }; ``` 2. 當傳入rvalue (`0`)時,`T`是rvalue (沒有reference),給`std::forward`的`T`是`int`,這和lambda傳入rvalue時的狀況不同,`decltype(x)`將會是rvalue ref ```cpp= // Apply int to template parameter T (when calling fwd(0)) void fwd(int && param) { func(std::forward<int &&>(param)); } // after ref collapse void fwd(int&& param) { func(std::forward<int>(param)); } // Apply int& to auto (when calling f(0)) auto f = [](int && x) { return func(std::forward<decltype(x)>(x)); }; // after ref collapse auto f = [](int&& x) { return func(std::forward<int&&>(x)); }; ``` 3. 雖然在傳入rvalue時兩者狀況不同,但對`std::forward`而言,`T`是`int`還是`int&&`並無差別,因此在lambda中使用`std::forward<decltype(x)>`是可以的 ```cpp= // std::forward implementation template<typename T> T&& forward(remove_reference<T>& param) { return static_cast<T&&>(param); } // Apply int to T (no need ref collapse) int&& forward(int& param) { return static_cast<int&&>(param); } // Apply int&& to T int&& && forward(int& param) { return static_cast<int&& &&>(param); } // After ref collapse // The same as passing int to T int&& forward(int& param) { return static_cast<int&&>(param); } ``` ## Item 34: Prefer lambdas to std::bind lambda相較於`std::bind`有許多好處,更有可讀性,表達性更高,且更有效率 考慮以下例子,有一function `setAlarm`,會在某個時間點`t`,會觸發鬧鈴長達`d`的時間,音樂可以被設定 ```cpp= using Time = std::chrono::steady_clock::time_point; enum class Sound { Beep, Siren, Whistle }; using Duration = std::chrono::steady_clock::duration; // alarm at time t, make sound s for duration d void setAlarm(Time t, Sound s, Duration d); ``` 接著,我們希望有個function,是可以設定alarm在1小時後觸發,duration 30秒,但是聲音是什麼,由參數決定 在lambda中可以用以下寫法,當呼叫`setSoundL`時,會設定一個鬧鈴,在1小時後觸發,達30秒,聲音是使用參數`s` ```cpp= // C++ 11 version auto setSoundL = [](Sound s) { using namespace std::chrono; // alarm to go off in an hour for 30 seconds setAlarm(steady_clock::now() + hours(1), s, seconds(30)); }; // C++ 14 version auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; // for C++ 14 suffixes // alarm to go off in an hour for 30 seconds setAlarm(steady_clock::now() + 1h, s, 30s); }; ``` 如果用`std::bind`,可能會用以下做法 ```cpp= using namespace std::chrono; using namespace std::literals; using namespace std::placeholders; auto setSoundB = std::bind( setAlarm, steady_clock::now() + 1h, // incorrect _1, 30s ); ``` 1. `placeholders::_1`表示`setAlarm`中對應的的參數是未知,將會由`setSoundB`的第一個參數做取代,由於placeholder不帶參數,呼叫`setSoundB`時若不確定型態,就必須再確認`setAlarm`中對應的參數型態是什麼,與lambda相比表達性較低 2. 由於`steady_clock::now() + 1h`是作為`std::bind`的參數,會在`std::bind`呼叫時被evaluate,因此timer會在呼叫`std::bind`時就啟動,並在呼叫`std::bind`之後1小時後觸發鬧鈴,而不是`setSoundB`被呼叫的時候,這是不正確的行為 要用`std::bind`實作正確的行為,就要推遲evaluate `steady_clock::now() + 1h`的時間點,將evaluate的行為用`std::bind`包裝成另一個function object就可做到 但相較於lambda,顯得十分麻煩,也不易閱讀 ```cpp= auto setSoundB = std::bind( setAlarm, std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s ); ``` 注意在C++ 14後,可以用類似`std::plus<>`,不指定template parameter,這會推導為`std::plus<void>`,此時STL會根據參數推導回傳值型態 C++ 11不支援,必須要用`std::plus<steady_clock::time_point>` 如果`setAlarm`有其他overloading的function,lambda的優勢會更顯著 假設有另一`setAlarm`,多接收一個參數代表音量,如下例 ```cpp= enum class Volume { Normal, Loud, LoudPlusPlus }; void setAlarm(Time t, Sound s, Duration d, Volume v); ``` lambda版本可以正確運作,因為lambda版本是直接呼叫function,可以正確解析為3個參數的版本 但`std::bind`版本不行,因為傳入`std::bind`時,只有function name,compiler無法推斷其為3個參數的版本還是4個參數的版本 ```cpp= auto setSoundL = [](Sound s) { using namespace std::chrono; setAlarm(steady_clock::now() + 1h, s, 30s); //ok. call 3-arg version }; auto setSoundB = std::bind( setAlarm, // error. which setAlarm should use std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s ); ``` 要解決此問題,必須明確指明function type,將其轉為function pointer 但由於被轉型為function pointer,此時`std::bind`行為將些許不同 產生的bind object會存function pointer,指向`setAlarm`,而不是直接呼叫該function 因此效率上可能會較差 ```cpp= using SetAlarm3ParamType = void(*)(Time, Sound, Duration); auto setSoundB = std::bind( static_cast<SetAlarm3ParamType>(setAlarm), std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s ); ``` 另外,若不了解`std::bind`實作方式,可能會對參數傳遞的方式有所誤解 1. 若用到placeholders (`_1`、`_2`...),實際呼叫bind object時需要傳入對應的參數,該參數會用by reference方式傳遞 (因為bind object用perfect forward) 2. 若呼叫`std::bind`時,傳入一些參數做綁定,會用copy (或move,如果是rvalue)方式存入bind object的data member 3. 這在lambda中都可以明確表示,一目了然 ```cpp= // compression level on Widget enum class CompressLevel { Low, Normal, High }; // function to make compress on Widget w Widget compress(const Widget& w, CompressLevel level); Widget w; // w is passed by value to compressB auto compressB = st::bind(compress, w, _1); // w is captured by value to compressL auto compressL = [w](CompressLevel level) { return compress(w, level); }; // CompressLevel::High is passed by reference compressB(CompressLevel::High); // CompressLevel::High is passed by value compressL(CompressLevel::High); ``` NOTE: 若要把`Widget w`以by ref方式傳給`std::bind`,必須要`std::ref`才可以 ```cpp= // w is passed by ref to compressB auto compressB = st::bind(compress, std::ref(w), _1); ``` `std::bind`在C++ 14中可以完全被lambda取代,但在C++ 11中依然有必要性 1. Move capture: 如[Item 32](Item-32-Use-init-capture-to-move-objects-into-closures),`std::bind`可用在需要做move capture的情境上 2. 多型函式物件:由於bind object的`operator()`是用perfect forward將參數傳給function object,因此可以接受任何參數。如果想將bind object和function object的template版本`operator()`聯繫在一起,`std::bind`就會很有用 ```cpp= struct PolyWidget { template<typename T> void operator()(const T& param); }; PolyWidget w; auto boundB = std::bind(w, _1); boundB(1930); // pass int to PolyWidget::operator() boundB(nullptr); // pass std::nullptr_t to PolyWidget::operator() boundB("abcde"); // pass string literal to PolyWidget::operator() // C++ 14 only auto boundL = [w](const auto& param) { w(param); }; ``` # Chapter 7. The Concurrency API ## Item 35: Prefer task-based programming to thread-based 若要非同步的執行任務,C++ 11提供兩種方式 1. thread based: 用`std::thread` 2. task based: 用`std::async` ```cpp= int doAsyncWork(); // thread based std::thread t(doAsyncWork); auto fut = std::async(doAsyncWork); ``` 相比於thread based,用task based可以有幾個好處 1. 由於`doAsyncWork`有回傳值,用`std::thread`沒有直接方式取得回傳值,但`std::future`有`get()` function可以取得 2. 如果`doAsyncWork`會拋出例外,也可以從`get`取得,但若用`std::thread`,程式會呼叫`std::terminate`,終止執行 3. 用`std::async`可以忽略開thread的細節,因為`std::thread`是software thread的抽象化物件,代表底層software thread的handle (e.g. pthread的`pthread_t`) a. 若create`std::thread`超過了software thread的上限,會拋出`std::system_error`的exception (與`doAsyncWork`無關,即使`doAsyncWork`是`noexcept`也有可能) b. 直接開thread,也有可能會oversubscript,即software thread超出hardward thread的狀況,導致需要context switch,cache flush等狀況,若要最佳化這樣的情況,要時時掌握程式的執行狀況,也需要了解許多硬體細節 4. `std::async`則是將開thead的策略交給STL,使用`std::async`不保證真的會create thread (如系統負荷高時),此時執行`doAsyncWork`的時機點會變成呼叫`std::future::get`的時候才執行 雖然用`std::async`有許多好處,但也有適合用`std::thread`的時機 1. 需要存取底層thread實作API時:由於C++ concurrency API多是透過底層平台的API實作(e.g. pthread),C++ concurrency API功能還不夠豐富,不足以cover底層平台如pthread的實作。若要使用pthread其他功能,還是只能用`std::thread` 2. 需要且可以最佳化thread執行狀況時:多是有使用特殊硬體的狀況時 3. 需要實作C++ concurrency API無法提供的thread framework時:例如thread pool ## Item 36: Specify std::launch::async if asynchronicity is essential `std::async`提供一個參數`policy`表示STL應該如何執行function `f` `policy`可以是 1. `std::launch::async`:表示function `f`必須以非同步的方式執行,也就是會create new thread執行 2. `std::launch::deferred`:表示function `f`會延遲到`std::future::get`或`std::future::wait`被呼叫時才被執行,不會create new thread,而是在呼叫`get`或`wait`的thread上執行 3. `std::launch::async | std::launch::deferred`:此為預設policy,表示交給系統決定要用哪種執行方式 預設policy使得`std::async`可能以同步或非同步的方式執行,雖然有彈性,但也會造成一些問題 ```cpp= auto fut = std::aync(f); // suppose program creates fut in thread t1 // suppose f is run in thread t2 fut.get(); // suppose program gets the result in thread t3 ``` 以上情境,假設程式在`t1` 用`std::async`create了`fut`,並在`t3`執行`fut.get`,而`f`在`t2`被執行 若用預設的policy,則 1. 無法預測`t1`和`t2`是否會並行處理 2. 無法預測`t2`和`t3`是否是相同 3. 無法預測function `f`是否執行 這狀況導致此policy無法與`thread_local`並存,若function `f`中有用到`thread_local`的物件,`f`不知道他用到的是`t2`的變數,還是`t3`的變數 而如果用`std::async`後,會使用`std::future::wait_for`或`std::future::wait_until`等timeout機制的話,有可能造成程式永遠無法結束 ```cpp= using namespace std::literals; void f(); auto fut = std::async(f); // will wait forever if std::launch::deferred is applied while (fut.wait_for(100ms) != std::future_status::ready) { ... } ``` 解決方式是確認`fut.wait_for` (或`wait_until`)的回傳值是否為`std::future_status::deferred`,若是該值,表示結果只有在明確呼叫`get`或`wait`後才會得到 以下用`wait_for(0s)`來確認function是否被延遲執行 ```cpp= auto fut = std::async(f); if (fut.wait_for(0s) == std::future_status::deferred) { // call fut.get() } else { while (fut.wait_for(100ms) != std::future_status::ready) { ... } } ``` 若想保證`std::async`一定要非同步執行,要用`std::launch::async` flag 這也可以包成一個template function ```cpp= // C++ 11 version template<typename F, typename... Ts> inline std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params) { return std::async( std::launch::async, std::forward<F>(f), std::forward<Ts>(params)... ); } // C++ 14 version template<typename F, typename... Ts> inline auto reallyAsync(F&& f, Ts&&... params) { return std::async( std::launch::async, std::forward<F>(f), std::forward<Ts>(params)... ); } ``` ## Item 37: Make std::threads unjoinable on all paths `std::thread`有joinable和unjoinable兩種狀態 unjoinable的thread可能是: 1. Default constructed `std::thread` 2. Moved `std::thread`:被move的thread不與底層thread有關聯 3. 已被joined的`std::thread` 4. 被detached的`std::thread` `std::thread`的destructor會檢查狀態是否是joinable,如果是,會呼叫`std::terminate`強致終止程式 ```cpp= bool doWork() { int var = 0; std::thread t( [&var] { // do something ... } ); if (conditionsAreSatisfied()) { t.join(); ... return true; } return false; } ``` 1. thread `t`在construct之後被執行 2. 若`conditionsAreSatisfied()`是`true`,程式沒問題 3. 若是`false`,thread會在沒有被join的狀態下被destruct,會呼叫`std::terminate` 在`std::thread`沒有被join的狀態下被destruct是危險的,C++標準委員會認為此種狀況下,不論在destructor中呼叫`join()`或是`detach()`,都不正確 1. 在destructor中呼叫`join()`,會導致程式被block,造成效能問題,且這問題可能難以被追蹤 2. 在destructor中呼叫`detach()`,可能導致stack corrupt。如上例中,thread `t` capture區域變數`var`,並可能在function中修改`var`,若在離開`doWork`時`detach`,`var`被銷毀,但thread還持續使用stack上的同一塊記憶體,這會導致下一個被呼叫的function上某些變數被意外的修改 綜上所述,必須要確保當`std::thread`被銷毀前,已經被join 如果要保證在`std::thread`被銷毀前要呼叫`join`或`detach`,可以用RAII技巧包裝`std::thread` ```cpp= class ThreadRAII { public: enum class DtorAction { Join, Detach, }; ThreadRAII(std::thread&& t, DtorAction a) : action(a) , t(std::move(t)) { } ~ThreadRAII() { if (t.joinable()) { if (action == DtorAction::Join) { t.join(); } else { t.detach(); } } } private: DtorAction action; std::thread t; }; ``` 1. 在constructor中,`std::thread`參數是rvalue ref,因為`std::thread`只能move 2. destructor中,確認若t還沒有被join,就依據設定做`join`或`detach` ## Item 38: Be aware of varying thread handle destructor behavior 代表一個non-defered `std::future`與`std::thread`一樣,底層都有對應到一個software thread,但`std::future`的destructor根據狀態不同,行為會不相同 `std::future`是代表communication channel的接收端(caller端) 如下圖: ![](https://i.imgur.com/FF3wH7o.png) 1. 通常callee端會將運算的結果寫到channel的物件中(e.g. `std::promise`),caller從`std::future`接收 2. 在中間會有一個shared state (通常在heap上),這是因為callee和caller兩邊可能非同步執行 例如callee的運算結果可能在`std::future::get`呼叫前就算好了,其值若存在callee,在callee端物件被銷毀時結果就消失了 3. 運算結果也無法存在caller的`std::future`,因為`std::future`可能被copy (`std::shared_future`),要存結果會不知道存在哪個`future`中 4. callee端代表物件有可能是`std::promise`,也可能是`std::async`的internal object 5. `std::future`要馬從`std::promise`取得,要馬從`std::async`的回傳值取得,因此shared state要馬是從`std::promise`產生,要馬從`std::async`產生 `future`的destructor會依據shared state的狀況有關 1. 當所有以下條件滿足時,`future`的destructor會自動`join`到非同步的thread,直到任務結束: a. 該`future`所對應到的shared state是從`std::async`產生的 b. 該任務是非同步執行的 (明確指定`std::launch::async`、或者用預設policy且系統決定非同步執行) c. 該`future`是最後一個指向shared state的 2. 其他狀況,destructor只是銷毀`future` object 由於`future`的API沒有提供機制來判斷shared state是否由`std::async`產生,對任意的`future` object,無法確定destructor是否會`join` `std::future`不一定是由`std::async`產生,也可以透過如`std::package_task`產生 ```cpp= // function to run asynchronously int calcValue(); { // enter scope std::package_task<int()> pt(calcValue); auto fut = pt.get_future(); // create thread from the package_task std::thread t(std::move(pt)); // process ... ... } // leave scope ``` 1. `package_task`只能move,不能copy,因此要傳給`std::thread`時,要用`std::move` 2. `fut`不是用`std::async`產生,而是從`package_task`,因此destructor不會join 3. 在create thread `t`以後直到leave scope,`t`狀態有可能是 a. `t`沒有被`join`也沒有被`detach`,此時`t`的destructor會呼叫`std::terminate` b. `t`被`join`,此狀況不需要在`future`的destructor去`join`,因為caller端已經`join` c. `t`被`detach`,此狀況不需要在`future`的destructor去`detach`,因此caller端已經`detach` 從上三種狀況可知,當`future`來源不是`std::async`時,所有狀況都已經由caller端處理好了,因此`future`的destructor可以不做事 ## Item 39: Consider void futures for one-shot event communication 有時候,不同thread之間需要做event notification。一個thread執行到一個階段後,會通知另一條thread,讓另一thread可以繼續執行 event notification是condition variable典型使用例子 如下,假設detecting task表示該task用於偵測條件是否滿足,reacting task表示該task會接收notification並且做對應的task ```cpp= std::condition_variable cv; std::mutex m; bool flag = false; // detecting task void detect_task { ... // detecting event // after condition hit { std::lock_guard<std::mutex> g(m); flag = true; // set to true to tell reacting task something happen } cv.notify_one(); // notify reacting task } void react_task { ... { std::unique_lock<std::mutex> lk(m); cv.wait(lk, [] { return flag; }); // cv.wait(...) the same as following // while (!flag) // { // cv.wait(lk); // } // react the event (when lock is held) ... } // react the event (when lock is not held) ... } ``` 1. condition variable僅用於notification,條件本身用`bool flag`表示 2. `cv.wait(lk, [] { return flag; })`是必須的,lambda表示predicated condition,設定lambda時,等同於註解的`while`迴圈 3. predicated condition是為了 a. 避免`wait`時有spurious wakeup (雖然被喚醒了,但條件其實不滿足),因此用迴圈去check條件是否滿足,若不滿足則繼續`wait` b. 避免條件在wait之前就已成立(detecting task在thread執行reacting task時就已notify),此情況下去`wait`可能造成永遠不會被喚醒,因此`wait`之前要先check condition 雖然condition variable可以滿足event notification的使用,但寫起來複雜 而像上例中,detecting task其實只是想通知有event發生,沒有傳遞任何其他資料給reacting task 理論上不需要`bool flag` (`cv.notify_one`就足夠代表有event發生) `bool flag`僅是為了應對thread之間可能發生的同步問題而存在 如果使用情境滿足以下狀況時,可以用void future代替condition variable 1. event notification只會發生一次 2. detecting task僅僅是通知有event發生,除此之外不會傳遞任何資料給reacting task 概念如下 ```cpp= std::promise<void> p; // detecting task ... // detecting event p.set_value(); // notify // reacting task p.get_future().wait(); // wait for the notification ... // react the event ``` 1. detecting task用`std::promise`作為communication channel的寫入端,reacting task用`std::future` 2. 由於兩thread之間沒有資料傳遞,僅是通知event發生,因此`std::promise`和`std::future`的template參數是`void`。即為`std::promise<void>`和`std::future<void>` 3. 當要做notification時,呼叫`std::promise::set_value`,通知對應的`std::future` 4. reacting task用`std::future::wait`等待event發生 5. 此寫法不用處理spurious wakeup以及`wait`前就滿足條件的問題,因為`std::future::wait`都幫我們處理好了 6. 此用法缺點是僅能通知event一次,condition variable可以反覆設定`bool flag`來多次通知event,但`std::promise::set_value`只能被設定一次 以下是使用的例子,假設我們想要建立一條thread,在執行thread function前先suspend,以便於我們先做一些初始化動作 ```cpp= std::promise<void> p; void react(); // function of reacting task void detect() // functino of detecting task { std::thread t( [] { p.get_future().wait(); // suspend thread t until notification react(); // do actual work } ); ... // do some init work. // At this moment thread t is suspend by std::future::wait // risky p.set_value(); // notify thread t. // After this thread t will call react() ... // remain work // risky t.join(); } ``` 1. thread `t`被create出來後就會開始執行,但是會被`std::future::wait`擋住 2. create完thread `t`後,我們先進行一些初始化工作,直到`p.set_value()`被呼叫前 3. 做完後呼叫`p.set_value()`,這會讓thread `t`繼續執行 4. 最後要呼叫`t.join()`,保證thread `t`可以被join 5. 在程式中中註解兩個risky的地方,如果在此二處`detect`拋出例外,會導致thread `t`在沒有被`join`的狀態下呼叫destructor,此時程式會呼叫`std::terminate`結束 ([Item 37](#Item-37-Make-std::threads-unjoinable-on-all-paths)) 可以用[Item 37](#Item-37-Make-std::threads-unjoinable-on-all-paths)介紹的`ThreadRAII`來確保thread `t`離開`detect()`的所有路徑都是unjoinable ```cpp= void detect() { ThreadRAII tr( std::thread( [] { p.get_future().wait(); react(); } ), ThreadRAII::DtorAction::join ); ... // risky p.set_value(); ... } ``` 用`ThreadRAII`後,不會有`std::terminate`的危險,但是卻產生另一種風險 程式中註解的risky地方,如果`detect()`在此處拋出例外,會導致`p.set_value()`不被呼叫 thread `tr`就會一直被卡在`p.get_future().wait()` 在此狀態下呼叫`ThreadRAII`的destructor,由於我們設定結束時要join,此時destructor就會永遠等下去,造成deadlock 此問題在[ThreadRAII + Thread Suspension = Trouble?](http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html)有更多討論 在原先不用`ThreadRAII`的版本,可以擴充為一次notify多個thread,這只要將`std::future`改為`std::shared_future`即可 ```cpp= std::promise<void> p; void react(); void detect() { std::shared_future<void> sf = p.get_future().share(); std::vector<std::thread> vt; for (int i = 0; i < threadsToRun; i++) { vt.emplace_back( [sf] // Copy sf { sf.wait(); react(); } ); } ... p.set_value(); ... for (auto& t : vt) { t.join(); } } ``` 需要注意在每個thread都需要copy `std::shared_future` ## Item 40: Use std::atomic for concurrency, volatile for special memory `std::atomic`與`volatile`是兩種不同的目的 `std::atomic`表示對該物件的read、write或RMW皆為atomic `volatile`無此保證 如下例子,假設Thread1與Thread2同時執行 ```cpp= std::atomic<int> ac(0); volatile int vc(0); /*--- Thread 1 ---*/ /*--- Thread 2 ---*/ ++ac; ++ac; ++vc; ++vc; ``` 當兩條thread執行結束後,`ac`的值保證是2,而`vc`則不一定,因為對`vc`的操作可能不是atomic,thread1和thread2同時寫入時,可能會互相覆蓋 e.g. 1. Thread1 read `vc` ,得到0 2. Thread2 read `vc`,得到0 3. Thread1 write `vc`,寫入1 4. Thread2 write `vc`,寫入1 另一差別是對程式碼的reorder e.g. ```cpp= a = b; x = y; ``` 由於兩個assignment沒有相互關係,編譯器可以任意調整順序 即使編譯器不調整,底層硬體處理時,也有可能會調整順序 使用`std::atomic`時,可以限制編譯器與硬體 ```cpp= // atomic std::atomic<bool> valAvailable(false); auto imptValue = compute(); valAvailable = true; // volatile volatile bool valAvailable(false); auto imptValue = compute(); valAvailable = true; ``` 使用`std::atomic`時,可以保證所有thread看到`imptValue`的新數值的時間點,不可以晚於`valAvailable`更新的時間點,也就是當看到`valAvailable`設為`true`時,`imptValue`已經是`compute()`的結果 用`volatile`時無此保證,順序可能被任意調動 NOTE: `std::atomic`對reorder的限制提供許多選項,預設是`std::memory_order_seq_cst`,表示不管讀寫都要一致 `volatile`用途是告訴編譯器,其處理的並不是一般的記憶體 一般的記憶體具有以下特性: 1. 當數值寫入記憶體位置後,直到被覆寫前都會保有相同的數值 如以下例子中,`x`被重複讀取,理論上`y`兩次assignment都會得到同樣的值 因此編譯器可以對此做最佳化,消除重複的read ```cpp= int x; auto y = x; // read x y = x; // read x again ``` 2. 如果數值被寫入記憶體位置後,從沒有讀取,接著再寫入另一個數值進去,就會清除原本寫入的值 如下例子,`x`被重複寫入,理論上第二次的值會覆蓋第一次,因此編譯器可以省去第一行 ```cpp= x = 10; // write x x = 20; // write x again ``` ```cpp= auto y = x; y = x; x = 10; x = 20; ``` 編譯器可以最佳化為 ```cpp= auto y = x; x = 20; ``` 使用`volatile`會避免編譯器做這種最佳化,這可能是因為該記憶體位置實際上是一個memory mapped I/O,例如某硬體的register ```cpp= auto y = x; // 1st read y = x; // 2nd read z = 10; // 1st write z = 20; // 2nd write ``` 若`x`代表溫度計的值,則第一次讀取和第二次讀取就有可能會不同 而同樣若`z`表示一個register,則對register寫入的可能就是一個命令,若省略第一次write,就有可能導致命令錯誤 這種避免最佳化重複載入貨無效儲存的特性,不適用於`std::atomic` ```cpp= std::atomic<int> x; std::atomic<int> y(x.load()); y.store(x.load()); ``` 可能被最佳化為 ```cpp= int register = x.load(); std::atomic<int> y(register); y.store(register); ``` NOTE: `volatile`不僅是可以用在memory mapped I/O,在一些如single handler/ multiple thread上也有可能可以使用 如以下例子 ```cpp= // test.cpp #include <thread> #include <chrono> int global; std::thread t; void func() { t = std::thread( []() { std::this_thread::sleep_for(std::chrono::seconds(5)); global = 1; } ); } int main() { func(); while (global == 0) { } } ``` 若`global`不是`volatile`,當編譯時,其可能會認為`global`的值不會改變,因此對`while`做最佳化,導致無窮迴圈 以下是用`-O3`編譯後,可能產出的assemble code 在`100000c79`處,比較`global`和`0` 在`100000c80`處確認,如果兩者相等(`while`條件成立),跳至`100000c90` 條件不成立,則繼續執行,最後會到`100000c85`執行retq,結束程式 而條件成立時跳到`100000c90`,就是進入無窮迴圈,該行指令就是反覆跳到同一個位置 ```bash= $ g++ -O3 -o test test.cpp $ objdump --disassemble test 0000000100000c70 _main: 100000c70: 55 pushq %rbp 100000c71: 48 89 e5 movq %rsp, %rbp 100000c74: e8 27 00 00 00 callq 39 <__Z4funcv> 100000c79: 83 3d f0 13 00 00 00 cmpl $0, 5104(%rip) 100000c80: 74 0e je 14 <_main+0x20> 100000c82: 31 c0 xorl %eax, %eax 100000c84: 5d popq %rbp 100000c85: c3 retq 100000c86: 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax) 100000c90: eb fe jmp -2 <_main+0x20> 100000c92: 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax) 100000c9c: 0f 1f 40 00 nopl (%rax) ... ``` 如果`global`變數是`volatile`,可能產出以下程式碼 在`100000c90`處`global`與`0`做比較 `100000c97`處,若等於`0` (`while`條件成立),跳回`100000c90`,繼續做`cmpl` 否則繼續執行,最終執行`retq` ```bash= $ g++ -O3 -o test main.cpp func.cpp $ objdump --disassemble test 0000000100000c80 _main: 100000c80: 55 pushq %rbp 100000c81: 48 89 e5 movq %rsp, %rbp 100000c84: e8 17 00 00 00 callq 23 <__Z4funcv> 100000c89: 0f 1f 80 00 00 00 00 nopl (%rax) 100000c90: 83 3d d9 13 00 00 00 cmpl $0, 5081(%rip) 100000c97: 74 f7 je -9 <_main+0x10> 100000c99: 31 c0 xorl %eax, %eax 100000c9b: 5d popq %rbp 100000c9c: c3 retq 100000c9d: 0f 1f 00 nopl (%rax) ``` # Chapter 8. Tweaks ## Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied 某一些function就是複製參數,為了效率,通常會分成兩種版本,一種接受lvalue,一種接受rvalue 有3種寫法可以達成 ```cpp= // version1 class Widget { public: void addName(const std::string& newName) { names.push_back(newName); } void addName(std::string&& newName) { names.push_back(std::move(newName)); } std::vector<std::string> names; }; // version2 class Widget { public: template<typename T> void addName(T&& newName) { names.push_back(std::forward<T>(newName)); } std::vector<std::string> names; }; ``` 1. 傳統方式會分成兩種function。缺點是要維護兩種僅有些微差異的function 不論何種function,在caller的參數傳到function時,都會bind在`newName` reference,不會有成本 而要複製進`names`時: 傳入lvalue時,會copy 1次 傳入rvalue時,會move 1次 2. 用universal reference,雖然只要維護一份function,但是用universal ref就需要維護template function,而且使用universal ref,使得function可以接受任意參數 同第1種方式,caller參數傳遞時,沒有成本 要複製進`names`時: 傳入lvalue時,會copy 1次 傳入rvalue時,會move 1次 本同款提供第3種方式,用by value方式傳遞參數 ```cpp= // version 3 class Widget { public: void addName(std::string newName) { names.push_back(std::move(newName)); } }; ``` 1. 此方式相較前兩種,程式碼只有一份,且維護也將對容易 2. 如果caller傳lvalue,在參數傳遞時會copy 1次,如果傳rvalue,會move 1次 而要複製進`names`時,則會再move 1次。因此 傳入lvalue時,會copy 1次、move 1次 傳入rvalue時,會move 2次 此方式總計會比前兩種方式多出1次move 3. 只有當參數是copyable的,才考慮此方法,因為如果只能move的參數,就不需要考慮那麼多,只要用move就好 ```cpp= class Widget { public: void setPtr(std::unique_ptr<std::string>&& ptr) { p = std::move(ptr); } std::unique_ptr<std::string> p; } ``` 4. 只有當move成本很低時,才考慮此方法。因為此方法不論如何,都會多一次move,如果成本太高,就不如第一種方法 5. 只有在參數一定會被複製時,才考慮此方法。如果function會根據某些條件決定是否複製,就表示當決定不複製時,function就必須要負責destruct物件,function就必須付出destruct的成本,這是前兩種方式不需付出的代價 ```cpp= class Widget { public: void addName(std::string newName) { if (name.length() >= 10) { names.push_back(std::move(newName)); } // if not hit, newName needs to be destructed by addName() } }; ``` 若複製的方式是用construct,則滿足上述條件,可以使用,成本就是會多1次move 但若複製方式是assigment,則需要再分析,如下例子 ```cpp= class Password { public: Password(std::string pwd) : text(std::move(pwd)) { } void changeTo(std::string newPwd) { text = std::move(newPwd); } std::string text; }; ``` constructor是用construct來copy參數到text,這在上面已分析過 function `changeTo`則是用assigment來copy參數到text 考慮以下情境 ```cpp= Password p("this_is_a_very_long_password"); std::string newPassword = "short_password"; p.changeTo(newPassword); ``` 在`p.changeTo()`中,`newPassword`首先會先被copy進function 接著在function中,`newPwd`中被move給text,此動作首先會把text內部資料delete,接著才做move 對text多做一次資料釋放,在使用第一種方式時很可能是不需要的,因為原本text的長度夠長,使用本來的記憶體做copy就可以了 此狀況下,用move方式的成本包含了額外的記憶體配置(caller參數傳入)與釋放(`text`記憶體),比傳ref成本高上許多 ```cpp= class Password { public: void changeTo(const std::string& newPwd) { // can reuse the memory in text if text.capacity() >= newPwd.size() text = newPwd; } }; ``` 另外,若`text.capacity() < newPwd.size()`,兩種方式都需要釋放`text`的記憶體,速度相當 再者,by value方式容易受到slice problem的影響,如果function設計上,是接受某base class以及其derived class的話,就不能用by value方式 ```cpp= class Widget {}; class SpecialWidget : public Widget {}; // processWidget is designed to accept Widget and its derived type // But pass by value cause slice problem void processWidget(Widget w); SpecialWidget sw; processWidget(sw); ``` 綜上所述,利用by value方式傳遞參數,雖然可以簡化程式碼,只維護一份function 但使用時須符合多種條件,成本才不至於太高 分析包含了使用的container本身特性,傳入的參數特徵等等 需要實際量測並證明by value確實可以得到更好效率時,才考慮使用 ## Item 42: Consider emplacement instead of insertion STL container提供兩種方式來加入新元素,插入(`push_back`、`insert`)和定位(`emplace`) 插入法提供兩種overload,一種接受lvalue,一種接受rvalue ```cpp= template<typename T> class vector { public: void push_back(const T& x); void push_back(T&& x); }; ``` 傳入rvalue時,如 ```cpp= std::vector<std::string> v; v.push_back("xyz"); ``` 會先用`"xyz"`create出一個暫存的`std::string`物件(rvalue),再傳入rvalue版本的`push_back` function 接著在`push_back` function裡,將暫存物件bind到`x` reference,接著再使用move constructor把`x`複製進vector中 最後,離開`push_back`後,再呼叫destructor,把暫存物件解構 使用`emplace`的話,可以避免create與delete暫存物件:`v.emplace_back("xyz")` `emplace_back`使用perfect forward,將`std::string` constructor所需參數直接傳入,並用之建構`vector`的元素 理論上`emplace`會比`push_back`好,但實際上卻會依據傳入的參數型態、使用的container、插入或emplace的位置等等不同因素,而有不同的效能表現,因此還是要先量測數據後再做決定 以下是大致的原則,若能滿足以下條件,幾乎可確定emplace會比插入好 1. 數值以constuct方式而非assign方式加入container 如以下例子,`"xyz"`並非加在最後面,而是加在最前面,這種方式通常會將數值move到指定位置 由於move operator需要有物件來做為source,因此肯定要有暫存物件的產生 由於`emplace`優勢在於不用產生暫存物件,此狀況下`emplace`也必須被迫產生暫存物件,變得沒有優勢 ```cpp= std::vector<std::string> v; v.emplace(v.begin(), "xyz"); ``` 2. 傳入的參數型態與容器的元素形態不同 如果傳入的型態本身就和容器的元素相同,不管用`emplace`或是插入效果都是相同的 3. 當插入一個已經存在的值時,container也不會拒絕存入 這種類型的container有`std::vector`、`std::deque`等,不是這種類型的則如`std::set` container為了確認數值是否已經存在,通常要先建立節點,再進行比較 而`emplace`版本的function就必須建立暫存物件,失去優勢 考慮是否使用`emplace`,還有兩點要注意 1. 資源管理 ```cpp= std::list<std::shared_ptr<Widget>> ptrs; void killWidget(Widget*); // pass shared_ptr with customized deleter ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget)); ptrs.emplace_back(new Widget, killWidget); ``` 在呼叫`push_back`前,就已經先create暫存物件 使用`emplace`確實可以不用create暫存物件,但卻有 可能引發其他問題 a. `new Widget`後,被傳入`emplace_back` b. 在`emplace_back`內部,再配置節點,如果失敗,會拋出例外 c. 例外傳到`emplace_back`外部,在a所new出來的raw pointer沒人可以access,造成leak 2. 和`explicit` constructor的相互關係 ```cpp= std::vector<std::regex> regexes; // add nullptr to vector regexes.emplace_back(nullptr); // Cannot compile regexes.push_back(nullptr); // Cannot compile std::regex r = nullptr; ``` 在例子中,開發者可以用`emplace_back`把`nullptr`存入container中,但這行為不符預期,因為指標根本不是`std::regex` 而如果用`push_back`的話,會發現傳入`nullptr`會無法編譯 再進一步測試,可發現`std::regex`理論上不能用`nullptr`來初始化 這是因為`std::regex`的constructor `std::regex::regex(const char*)`有帶`explicit` keyword 而`push_back`或是用等號做初始化,屬於copy initialization,這種initialization方式不能用explicit constructor `emplace_back`則是把`nullptr`傳入function中,並用direct initialization方式產生物件 即用如`std::regex elem(nullptr)`的方式。此方式可以用explicit constructor ---