# 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一樣配置一塊新的記憶體

但若記憶體都是在stack上(如`std::array`),做move時,可以對每個data member都使用move操作,但假設data member不支援,那實際上成本也是和copy一樣
如下圖,當`aw1`要被move到`aw2`時,`std::array`會對每個element做move操作,如果`Widget`有支援move,那整體速度就可能會比copy快
但若`Widget`不支援move操作,則實際上還是會copy

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端)
如下圖:

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