# Effect Modern C++ Part 1 [Part 2](https://hackmd.io/@Xps7TehyQ1GqEs4toiKu4w/S1Invxpc_) # Chapter 1. Dedcuing Types ## Item 1: Understand template type deduction template中分兩種型態 ```cpp template<typename T> void f(ParamType param); f(expr); ``` 其中`T`為template type `ParamType`為參數型態 而型態推導,即是將`expr`推導為`T`與`ParamType` template參數推導包含以下種case * Case 1: `ParamType`為pointer或是reference,且不為universal reference * Case 2: `ParamType`為universal reference * Case 3: `ParamType`不為pointer也不為reference * Case 4: 當`expr`為Array type或Function type時 * Case 5: 當`T`為function signature的一部份時 Universal reference: * 或稱forwarding reference * 型態為`T&&`,不能包含cv-qualifier * `T`參與型態推導過程 ### Case 1: `ParamType`為pointer或是reference,且不為universal reference 推導規則: 1. 若`expr`的型態是reference,將`expr`的reference去掉 2. 接著把`expr`match到`ParamType`,並將`T`推導為對應的型態 E.g. ```cpp= template<typename T> void f(T& param); int x = 27; // x -> int const int cx = x; // cx -> const int const int& rx = x; // rx -> reference to const int f(x); // T -> int // ParamType -> int& f(cx); // T -> const int // ParamType -> const int& f(rx); // T -> const int // ParamType -> const int& ``` 請注意在第3例推導`rx`時,`T`將不會包含reference (原文: reference-ness is ignored during deduction. `T` is deduced to be non-reference) #### 若`expr`為rvalue reference,規則同樣適用。 ```cpp= template<typename T> void f(const T&& param); int x = 27; f(std::move(x)); // expr -> rvalue reference of int // T -> int // ParamType -> const int&& ``` 請注意此處參數並不是universal reference,因為其包含cv-qualifier #### 若`ParamType`包含qualifier,規則同樣適用 ```cpp= template<typename T> void f(const T& param); int x = 27; const int cx = x; const int& rx = x; f(x); // T -> int // ParamType -> const int& f(cx); // T -> int // ParamType -> const int& f(rx); // T -> int // ParamType -> const int& ``` #### 若參數為pointer,規則同樣適用 ### Case 2: `ParamType`為universal reference 推導規則: 1. 若`expr`為lvalue,則`T`和`ParamType`皆被推導為lvaue reference。<font color="#f00">此為唯一一條``T``被推導為lvalue reference的規則</font> 2. 若`expr`為rvalue,則apply [Case 1](#Case-1-ParamType為pointer或是reference,且不為universal-reference)的規則 ```cpp= template<typename T> void f(T&& param); int x = 27; const int cx = x; const int& rx = x; f(x); // expr -> int // T -> int& // ParamType -> int& f(cx); // expr -> const int // T -> const int& // ParamType -> const int& f(rx); // expr -> const int& // T -> const int& // ParamType -> const int& f(27); // expr -> rvalue // T -> int // ParamType -> int&& ``` ### Case 3: `ParamType`不為pointer也不為reference 此情況下,參數傳遞方式會是pass-by-value ```cpp template<typename T> void f(T param); ``` 1. 若`expr`為reference,將reference去掉 2. 若`expr`包含cv-qualifier,也將cv-qualifier去掉 ```cpp= template<typename T> void f(T param); int x = 27; const int cx = 27; const int& rx = x; f(x); // T -> int // ParamType -> int f(cx); // T -> int // ParamType -> int f(rx); // T -> int // ParamType -> int ``` * 請注意此例中,`ParamType`不包含cv-qualifier。這是合理的,對pass-by-value的參數加上cv-qualifier並無意義。 若`ParamType`包含cv-qualifier,則規則2改為將`expr`match到`ParamType`,並將`T`推導為對應的型態 * 此例中,不論`expr`是否包含cv-qualifier,結果都不包含cv-qualifier。 * 但若`expr`是pointer-to const,則會被保留 ```cpp= template<typename T> void f(T param); const char* p = "abc"; const char* const cp = "abc"; f(p); // T -> const char* // ParamType -> const char* f(cp); // T -> const char* // ParamType -> const char* ``` ### Case 4: 當`expr`為Array type或Function type時 因為Array type可以退化成pointer type,在推導處理上,會有些不同 ```cpp= const char a[] = "abcde"; // type of a is const char[6] const char* p = a; // array decays to pointer ``` * 當`expr`是Array type,而參數是pass-by-value時,Array會退化成pointer,再做推導 ```cpp= template<typename T> void f(T param); const char a[] = "abcde"; // type of a is const char[6] f(a); // expr -> const char[6] decays to const char* // T -> const char* // ParamType -> const char* ``` * 當`expr`是Array type,且參數為reference時,會推導為reference to array ```cpp= template<typename T> void f(T& param); const char a[] = "abcde"; // type of a is const char[6] f(a); // expr -> const char[6] // T -> const char[6] // ParamType -> const char(&)[6] ``` * Function type的推導規則和Array type類似 ```cpp= void someFunc(int, double); // type is void(int, double) template<typename T> void f1(T param); void f2(T& param); f1(someFunc); // T & ParamType -> void (*)(int, double) f2(someFunc); // T & ParamType -> void (&)(int, double) ``` ### Case 5: 當`T`為function signature的一部份時 template type `T`可以是function signature的一部份 ```cpp= template<typename T> void f(void(*param)(T)); ``` function `f`接收一個signature為`void(*)(T)`的function pointer為參數 此時`T`會完全match到function signature ```cpp= // accept function pointer with return type R and a parameter T template<typename R, typename T> void f(R (*param)(T)); void f1(int); void f2(int&); void f3(const int&); int f4(int); int& f5(int); const int& f6(); f(f1); // R -> void // T -> int // param -> void (*)(int) f(f2); // R -> void // T -> int& // param -> void (*)(int&) f(f3); // R -> void // T -> const int& // param -> void (*)(const int&) f(f4); // R -> int // T -> int // param -> int (*)(int) f(f5); // R -> int& // T -> int // param -> int& (*)(int) f(f6); // R -> const int& // T -> int // param -> const int& (*)(int) ``` ## Item 2: Understand `auto` type deduction `auto`的型別推導與[Item 1](#Item-1-Understand-template-type-deduction)描述的規則是相同的 Case 1 - Case 5都是一樣的 唯一不同的是當使用uniform initialization (大括弧)做初始化時,`auto`將會推導為`std::initializer_list` 在使用大括弧做初始化時,若無法被推導為`std::initializer_list`,則編譯會失敗 ```cpp= auto x1 = 27; // int auto x2(27); // int auto x3 = { 27 }; // std::initializer_list<int>, value is { 27 } auto x4 { 27 }; // std::initializer_list<int>, value is { 27 } //auto x5 { 27, 3.0 }; // Compile fail. Cannot be deduced ``` 這是唯一一個`auto`和template型別推導不同的地方 當`auto`碰到uniform initialization時,將推導為`std::initializer_list` 而template會拒絕推導,編譯失敗 ```cpp= template<typename T> void f(T param); template<typename T> void g(std::vector<T> param); // f({1, 2, 3}); // Compile failed. Cannot infer type of T // g({1, 2, 3}); // Compile failed. Cannot infer type of T ``` 要讓`f({1, 2, 3})`能夠成功編譯的方式,是讓`T`成為`std::initializer_list`的參數 ```cpp= template<typename T> void f(std::initializer_list<T> param); f({1, 2, 3}); // Ok. T is int. param is std::initializer_list<int> // Please note this is the only method to accept f({1, 2, 3}); // Others like std::vector<T> will reject to deduce such call ``` ## Item 3: Understand `decltype` `decltype`的參數是一個`expr`或是一個name,回傳其型態 ```cpp= int x = 5; // decltype(x) is int int& rx = x; // decltype(rx) is const int& const int cx = x; // decltype(cx) is const int bool f(const int& i); // decltype(i) is const int& // decltype(f) is bool(const int&) f(x); // decltype(f(x)) is bool ``` 請注意`decltype(x)`和`decltype((x))`是不同的 `x`是一個變數的名稱,因此`decltype(x)`將回傳`int` 但`(x)`是一個`expr`,C++定義`(x)`是一個lvaue,因此`decltype((x))`回傳`int&` ## Item 4: Know how to view deduced types 用boost library的TypeIndex可以在runtime顯示型態的名稱 ```cpp= #include <boost/type_index.hpp> using boost::typeindex::type_id_with_cvr; template<typename T> void f(const T& param) { cout << "T: " << type_id_with_cvr<T>().pretty_name() << "\n"; cout << "param: " << type_id_with_cvr<decltype(param)>().pretty_name() << "\n"; } ``` # Chapter 2. auto ## Item 5: Prefer auto to explicit type declarations `auto`的好處 ### 使用`auto`宣告變數時,該變數一定要被初始化 ```cpp= int x1; // Ok, but potentially uninitialized auto x2; // No. x2 must be initialized auto x3 = 0; // Ok ``` ### `auto`能減短冗長的型態,如iterator ```cpp= template<typename It> void f(It b, It e) { while (b != e) { // Long type typename std::iterator_traits<It>::value_type curr1 = *b; // Use auto auto curr2 = *b; // do something ... } } ``` ### 有些型態如lambda這種只有compiler知道的型態,僅能用`auto`宣告 ```cpp= auto compare = [](int p1, int p2) { return p1 < p2; }; ``` 請注意此處`compare`的型態不見得一定為`std::function<bool(int, int)>`,即lambda與`std::function`實際上是不同的東西 `std::function`為C++ STL中的容器,可以代表任何callable的object,例如function pointer,或者有定義`operator()`的class,上例中lambda的定義會產出一個callable的object,可以被`std::function`接收,因此也可以寫成 ```cpp= std::function<bool(int, int)> compare = [](int p1, int p2) { return p1 < p2; }; ``` 代表要產生一個`std::function`的物件,這當中會需要呼叫`std::function`的constructor,初始化一些member data,佔用一些記憶體 使用`auto`定義lambda,如何處理則交給compiler,因此通常使用`auto`會比用`std::function`有效率 ### 使用`auto`宣告,也可以避免type shortcuts,開發者誤用type,而導致的未定義行為 * 例子1 ```cpp= std::vector<int> v; unsigned sz = v.size(); // may cause overflow issue auto sz2 = v.size(); // sz2 is deduced from v.size(). No overflow issue ``` 上例中,`v.size()`回傳型態`std::vector<int>::size_type`,這是一個unsigned integral type,也因此開發者會自然的寫`unsigned`來接收回傳值 但這段程式碼在不同平台上,可能會有不同的行為 1. Windows 32-bit中,`unsigned`和`std::vector<int>::size_type`皆為32 bits,沒問題 2. Windows 64-bit中,`unsigned`是32 bits,而`std::vector<int>::size_type`為64 bits,造成溢位的可能 3. 若使用`auto`,可以避免此問題 * 例子2 ```cpp= std::map<std::string, int> m; // Wrong. key type should be const for (const std::pair<std::string, int>& p : m) { } // Correct for (const std::pair<const std::string, int>& p : m) { } // Correct, and shorter for (const auto& p : m) { } ``` 上例中的for loop中,我們通常認為`std::pair`的key type應該和宣告map時的key type相同,即為`std::string` 但實際上兩者是不同的,由於map中,key應為const,因此`std::pair`應寫為`std::pair<const std::string, int>` 此例中,因誤寫為`std::pair<std::string, int>`,compiler會將`std::pair<const std::string, int>`轉型為`std::pair<std::string, int>`,導致變數`p`變成一個temporary object,`p`會在foor loop的scope結束時被銷毀,若開發者對`p`取位址,將引發undefine behavior ## Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types 使用`auto`來定義proxy class/function的結果,可能會導致推導的型態與期望不符 ```cpp= // Return a vector of bool to represent if features are enabled // Each bit represents a feature // Suppose the 5th feature is about if it is high priority std::vector<bool> features(const Widget& w); // Process a widget based on if it is high priority void processWidget(Widget& w, bool isHighPriority); // Normal version Widget w; bool highPriority = features(w)[5]; // Ok. Get value of 5th bit processWidget(w, highPriority); // Ok // Auto version Widget w2; auto highPriority2 = features(w2)[5]; // Still ok // But highPriority2 is not bool processWidget(w2, highPriority2); // Undefined behavior ``` * proxy class: 用來模擬其他的class行為,目標是讓user可以在不明白細節的前提下,讓使用的結果就好像是沒有proxy一樣,例子是`std::vector<bool>::reference` * `std::vector<bool>::operator[]`的回傳值並不是`bool&`,而是`std::vector<bool>::reference`,這是一個可以隱性轉換為`bool`的內部class * 上例中,`highPriority2`的型態為`std::vector<bool>::reference`,此proxy class實作方式,通常是內部存了一個pointer,指向`std::vector<bool>`的內部結構,當轉換為`bool`時,利用該pointer,找到對應的bit * `features(w2)`回傳一個temporary object (暫稱`temp`),接著`features(w2)[5]`回傳`std::vector<bool>::reference`並存在`highPriority2`,該reference存了一個pointer,指向`temp`的內部結構 * 因`temp`是temporary object,Line 17結束後,`temp`即被銷毀,但`highPriority2`還存在,此時`std::vector<bool>::reference`內部存的pointer就變成dangling pointer,access `highPriority2`將導致未定義行為 要使用`auto`,又要解決此問題,可以使用`static_cast`強制轉型 ```cpp= auto highPriority2 = static_cast<bool>(features(w2)[5]); ``` * 除了`std::vector<bool>::reference`,也有許多其他的proxy class也是如此,例如`std::bitset::reference`,這類proxy class的特色是它的存在是invisible的,user可能會在不經意的情況下使用到proxy class * 藉由閱讀程式碼,可以確認底層library是否有使用proxy class,確認function回傳值是否是一個proxy class ```cpp= // From C++ Standards namespace std { template<class Allocator> class vector<bool, Allocator> { public: class reference { ... }; // Expect to return bool& // But actually it returns reference // This is a proxy class reference operator[](size_type n); } } ``` # Chapter 3. Moving to Modern C++ ## Item 7: Distinguish between `()` and `{}` when creating objects 新初始化方式Uniform initialization,用`{}`初始化 加入uniform initialization後,C++有以下方式初始化 ```cpp= Widget w; // default constructor Widget w1(); // NOT default constructor! This declares a function Widget w2(0, 1); // customized constructor Widget w3 = w; // copy constructor Widget w4(w); // copy constructor Widget w7 = {}; // default constructor or list initialization Widget w8 {}; // default constructor or list initialization Widget w9 {w}; // copy constructor or list initialization Widget w10 = {w}; // copy constructor or list initialization Widget w11 {0, 1}; // customized constructor or list initialization Widget w12 = {0, 1}; // customized constructor or list initialization ``` 從例子中可看到: 1. 在使用uniform initialization時,有無`=`並不影響 2. 第二行不會呼叫default constructor,而是會宣告一個function。此引用most vexing parse `Widget w();`有兩種解讀方式 a. 宣告一個變數`w`,型態為`Widget`並且不使用參數初始化 b. 宣告一個function w,回傳一個`Widget`物件,沒有function parameter C++會解譯為b 3. 當使用uniform initialization時,可能會呼叫到list initialization,這要看該class是否有提供list initialization constructor而定 當有list initialization constructor時,compiler會用盡各種方式嘗試使用之 a. 若型態恰巧相同,則呼叫list initialization constructor b. 若可隱性轉型,不發生narrowing conversion,呼叫之 c. 若可隱性轉型,但發生narrowing conversion,編譯失敗 d. 若有多個list initialization constructor,且有ambiguous,編譯失敗 e. 以上的rule也套用在copy, move constructor f. 以上都沒有,再去尋找非list initialization的constructor g. 唯一例外是使用`{}`,但大括弧中沒有任何參數,此時將呼叫default constructor,若沒有default constructor,才會找list initialization constructor ```cpp= struct A { A(int, bool); A(std::initializer_list<double>); }; A a { 0, true }; // call A(std::initializer_list<double>), apply rule b struct B { B(int, bool); B(std::initializer_list<bool>); }; B b { 10, true }; // error, apply rule c // int(10) convert to bool is narrowing conversion struct C { C(int, bool); C(std::initializer_list<double>); operator float() const; // Make C be allowed to convert to float }; C c; C c1 { c }; // call C(std::initializer_list<double>), apply rule e // c2 converts to float. // And float and implicitly convert to double C c2 {std::move(c)} // call C(std::initializer_list<double>) // the same as c1 struct D { D(int, bool); D(std::initializer_list<std::string>); }; D d { 10, true }; // call D(int, bool), apply rule f // int and bool cannot implicitly be converted to string struct E { E(); E(std::initilizer_list<double>); }; E e1 {}; // call E(), apply rule g // {} with empty set is treated as default ctor E e2 {{}}; // call E(std::initilizer_list<double>) E e3 ({}); // call E(std::initilizer_list<double>) struct F { F() = delete; F(std::initializer_list<double>); }; F f {}; // call F(std::initializer_list<double>) ``` Uniform initialization不允許narrowing conversion ```cpp= double x, yz; int sum1 { x + y }; // error int sum2(x + y); // ok int sum3 = x + y; // ok, same as sum2 ``` ### 結論 1. 作為class designer,需要考慮是否該提供list initialization constructor,若提供了,需要有強烈的提示讓使用者理解,如STL中的`std::vector`,提供list initialization constructor用來初始化vector需要有哪些元素,使用者可清楚明白何時該使用 2. 若原本class沒有list initialization constructor,而想要新增一個時,也要注意這可能會影響既有的程式碼,原本使用`{}`時呼叫的constructor,改完後可能會使用不同的constructor 03. 作為使用者,想用`{}`時要注意該class是否有提供list initialization constructor * Note: C++11提供新的member initialization,稱為default member initializer 要初始化member data有兩種方式,一種是在constructor中提供member initializer list,即以前的方式 另一種即為default member initializer,在宣告member data時,使用`=`或是`{}`來初始化 此情況下,當constructor沒有為該member提供member initializer list時,就會使用default member initializer ```cpp= struct S { int n = 7; // default member initializer S() {} // Since n is not included in initializer list // use default member initializer S(int x) : n(x) {} // Use x to initialize n }; ``` ## Item 8: Prefer `nullptr` to `0` and `NULL` `nullptr`的型態為`std::nullptr_t`,此型態可以隱性轉換為所有其他型態的pointer `NULL`在C++中型態為`0L`,當function是有pointer與int的overloading時,用`0`或`NULL`會有ambiguous,或者呼叫不預期的function 且當使用在template時,`0`和`NULL`將推導為實際型態`int`,而不是pointer,在template中用`nullptr`會被推導為`std::nullptr_t`,可以正確地將之看作pointer ## Item 9: Prefer alias declarations to `typedef` ```cpp= // typedef typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UP; // alias declaration using UP = std::unique_ptr<std::unordered_map<std::string, std::string>>; ``` 定義function pointer時較為清楚 ```cpp= // define FP as a function pointer // no return value, take int and const std::string& as parameter typedef void (*FP)(int, const std::string&); // alias declaration using FP = void (*)(int, const std::string&); ``` alias declaration可以用在template上,稱作alias tempalte E.g. 定義一個linked list,讓其使用客製化allocator `MyAlloc` ```cpp= // alias template template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>; // client code MyAllocList<int> list; // typedef version template<typename T> struct MyAllocList { typedef std::list<T, MyAlloc<T>> type; }; // client code MyAllocList<int>::type list; ``` alias template的優勢,在要使用dependent type時更加明顯 ```cpp= template<typename T> class Widget { // typedef version // Need to use typename to indicate MyAllocList<T> is a type typename MyAllocList<T>::type list; }; template<typename T> class Widget { // alias template MyAllocList<T> list; } ``` 上例中,`MyAllocList<T>::type`是dependent type,depend on `T`,因此需要加typename compiler才知道`MyAllocList<T>::type`是個type 會這樣是因為有可能在某些template特化中,`MyAllocList<T>::type`不是一個type e.g. 在下例中,`MyAllocList<Wine>::type`是個data member,而不是型態 ```cpp= // specialization for Wine type, the 'type' is a member data template<> class MyAllocList<Wine> { int type; }; ``` ## Item 10: Prefer scoped `enum` to unscoped `enum` C++ 98的`enum`是unscoped,`enum`裡的名稱的scope與`enum`的scope相同 ```cpp= enum Color { black, white, red }; // Scope of black and Color are the same auto black = false; // error. black already declared in this scope ``` scoped `enum`也叫enum class,scope不會跑到外面 ```cpp= enum class Color { black, white, red }; auto white = false; // ok Color c = white; // error. No enumerator named 'white' Color c = Color::white; // ok ``` 其他優點:不會隱性轉型成整數,可以forward declaration enum class預設是`int`型態,定義或宣告時,也可以自己選擇其他型態 ```cpp= // Color uses int as underlying type enum class Color {}; // Color2 uses uint32 as underlying type enum class Color2 : std::uint32_t {}; ``` 這只是選擇實作類型,但實際上enum class不能隱性轉換成整數 必需自己寫function轉換 但因為enum class不允許隱性轉換成整數,以往使用習慣上,將enum值當作array index的寫法就不work了 ```cpp= enum E { First, Second, }; // unscoped enum is ok int array[3]; array[First] = 0; array[Second ] 2; enum class E1 { First, Second, }; array[E1::First] = 0; // error ``` ## Item 11: Prefer deleted functions to private undefined ones 此通常用在會自動產生的member function ([Item 17](#Item-17-Understand-special-member-function-generation)). `delete` keyword明確表示某個function不可以被使用 通常deleted function會宣告為`public`,這是因為privilege check先於deleted check,若client access一個private deleted function,compiler會回報client沒有權限使用該function,而不是回報該function是deleted ```cpp= class A { public: A(const A&) = delete; // copy constructor is deleted }; ``` 此keyword亦可用在overloading function上,overloading resolution優於delete check,因此下例子中,`f(double)`會同時reject `double`和`float` ```cpp= bool f(int); bool f(double) = delete; // reject double and float f(0); // ok f(3.5); // error f(3.5f); // error ``` deleted function也可以運用在避免特定的template specialization ```cpp= // delete free function template<typename T> void processPointer(T* ptr); template<> void processPointer<void>(void*) = delete; // delete member function class Widget { public: template<typename T> void processPointer(T* ptr) {} } template<> void Widget::processPointer<void>(void*) = delete; ``` ## Item 12: Declare overriding functions `override` `override` keyword加在member function後面,表示是要override base class的virtual function 此keyword只有在被加在function後面時,才是keyword 因此在C++ 11,還是可以宣告一個member function為`override` `final` keyword也有同樣的特徵 ```cpp= class A { public: void override(); // legal void final(); // legal }; ``` * Note: C++ 11中提供新的function qualifier,叫做reference qualifier,此標明了該member function是否限定只能用lvaue reference呼叫,或只限定用rvalue reference呼叫 ```cpp= class A { public: using DataType = std::vector<double>; DataType& data() & { return values; } DataType&& data() && { return std::move(values); } private: DataType values; }; Widget w; auto val1 = w.data(); // call from lvalue. Call DataType& data() & // factory function A makeA(); auto val2 = makeA().data(); // call from rvalue. Call DataType&& data() && ``` 上例中,`A::data()`有兩種版本,`DataType& data() &`是lvalue版本,只有當object是lvalue時才能呼叫,另一種是`DataType&& data() &&`,只有rvalue才能呼叫 此overloading可以成立,就是因為兩個function的reference qualifier不同 此例子中若沒有這樣的overloading,可能會導致需要多次copy ```cpp= class A { public: DataType& data() { return values; } } auto val2 = makeA().data(); // call DataType& data() ``` ## Item 13: Prefer `const_iterator` to `iterator` C++ 11以後為`const` iteration 提供更完整的方案 STL container包含`cbegin`、`cend`、`crbegin`、`crend`,insertion之類的操作以及generic algorithm function(如`std::find`)也accept `const_iterator` C++ 11中還缺少的是non-member function `cbegin`和`cend`,這可以自己實作 ```cpp= template<typename C> auto cbegin(const C& container) -> decltype(std::begin(container)) { // Does not call container.cbegin() return std::begin(container); } ``` 實作中,我們需要的回傳值是`const_iterator`,而`std::begin`的參數若為`const &`的話,回傳值就會是`const_iterator`,符合我們的需求 這樣寫的好處是,array type也可以使用 有了這個function,就可以將此利用在template function上 有些container提供的`begin` (和其他`end`、`cbegin`、`cend`)不是member function版本,而是non-member function版本 以下的例子可以使用在member和non-member的container上 ```cpp= // Find first occurence of targetVal in container // and then insert insertVal there template<typename C, typename V> void findAndInsert(C& container, const V& targetVal, const V& insertVal) { // Allow ADL for both user-define and std // Both user-defined cbegin/cend and std::cbegin/cend can be found // std::cbegin/cend relies on C::cbegin/cend using std::cbegin; using std::cend; // non-member cbegin, cend // If C is a container providing member cbegin/cend, // std::cbegin/cend is used, and therefore C::cbegin/cend is called // If C is a container provding non-member cbegin is provided // That non-member function is called auto it = std::find(cbegin(container), cend(container), targetVal); container.insert(it, insertVal); } ``` ## Item 14: Declare functions `noexcept` if they won't emit exceptions 在C++ 11之後,舊的exception specification已經被遺棄 新的exception spec只有兩種分別:會拋出例外,以及不會拋出例外 如果一個function不會拋出例外,應該用`noexcept`標記 如果function沒有`noexcept`,表示該function可能會拋出例外 是否`noexcept`對client端來說是重要的,client端有可能會仰賴`noexcept`的語意做事情,或者藉由確認一個function是否`noexcept`來決定程式碼的效率 另外compiler也有可能會為`noexcept`的function產生更好的object code ```cpp= void f1() throw(); // no exception: C++ 98 style void f2() noexcept; // no exception: C++ 11 style ``` 假設在runtime時,`f1`拋出exception,compiler會保證stack會回朔到`f1`的caller,這包含`f1`中的區域變數會被destruct,回朔以後再呼叫`std::terminate`,讓程式結束 而如果`f2`在runtime拋出例外,compiler不保證會回朔,只保證會呼叫`std::terminate` 因此在`f2`上,compiler不見得需要保存回朔必要的資訊,因此有機會產生更快的object code 要注意不管是何種狀況,`f1`和`f2`最終都會呼叫`std::terminate`導致程式結束,`noexcept`是一種保證,compiler也仰賴此保證來產生程式碼,開發者須注意如果有`noexcept`,就要保證不會有任何exception被拋出 另一個`noexcept`的好處,是可能可以增進效能,例如`std::vector::push_back` 注意`push_back`保證如果執行時拋出例外,則`std::vector`不會有改變 由於新的element加入時,可能會因為空間不足,導致`std::vector`必須要配置新的更大的記憶體存放,此時`std::vector`必須逐一copy / move,把舊的資料放入新的空間中 如果是用copy的方式,可以有此保證,因為如果是當舊的element複製到新的空間時有問題,原有的資料不變,因此有保證 但如果是move的方式,當舊的element被move到新的空間時發生問題,原有的資料已經被改變,就不能保證了 STL解決這問題的方式,是在compiler time確認element是否有提供`noexcept`的move operation,如果有提供的話,才使用move,否則一樣會copy 因此,如果有提供`noexcept`的move,rvalue版本的`push_back`就會得到move的好處,效能可能就比較快 `noexcept`可以包含條件,例如一個`pair`的`swap()`是否是`noexcept`,取決於其第一個elem是否`noexcep`,以及第二個elem是否`noexcept`,可以寫成如下 ```cpp= template<class T1, class T2> struct pair { void swap(pair& p) noexcept( noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)) ); }; ``` 雖然`noexcept`可以帶來極大好處,但事實上大多數function都是exception-neutral的,本身不會拋出例外,但不保證內部呼叫的function不會拋出例外 此種function會將exception向上傳遞,直到有人`catch`。 此種function也不具備`noexcept`的特性 但如果有些function可以自然的變成`noexcept` (例如簡單的運算function),或是使用`noexcept`後可以帶來極大的好處 (如`swap`),值得盡可能地用`noexcept`實作 但如果為了迎合`noexcept`而過度改變實作,導致介面變得複雜 (如捕捉所有的exception並用狀態碼回傳),會導致維護困難 C++ 11之後,所有的`delete`,`delete[]`和destructor都是`noexcept`,不論使用者是否有明確指定 唯一例外是明確指明destructor是`noexcept(false)` 接著是`noexcept`在wide contract和narrow contract function的適用度 wide contract function意思是該function不假設任何前提,給任意的input都可以work,不會產生UB 不是wide contract的function就是narrow contract,此類function對input有假設 (如pointer不會是`nullptr`),如果給了假設以外的input,會造成UB 對於wide contract function,如果該function自然實作下,就不會拋出例外,則可用`noexcept` 對於narrow contract,根據定義此類function都假設input是合理的,因此如果自然實作下也不會拋出例外,其實也可以用`noexcept` 但是很多時候,此類function會對假設條件做檢查(如check pointer是否是`nullptr`),對於違反條件的input,丟出exception是很合理的 因此許多時候,narrow contract function不會有`noexcept` ## Item 15: Use `constexpr` whenever possible `constexpr`可以修飾object或function 當修飾object時,代表該object必須是要在compile time時可以被evaluate 當一個object是`constexpr`時,必為`const` 但反過來不一定成立,當一個object是`const`時,不一定為`constexpr` 被`constexpr`修飾的整數,可以被用在如定義array size、integral template argument等地方 ```cpp= constexpr int x1 = 5; // imply const const int x2 = 5; // imply constexpr constexpr x3 = x2; // ok. because x2 implies constexpr int x4 = 5; const int x5 = x4; // const, but not constexpr ``` 因此,當想要一個object其值必須要是compile time決定時,應使用`constexpr` 當`constexpr`修飾一個function時,則有不同的解釋 當一個`constexpr` function被呼叫時,若其參數是compile time時可以被決定的const value時,該function call的結果會在compile time時被決定 若其參數是runtime時才能決定,則行為如一般function一樣,在runtime時才會執行 ```cpp= constexpr int f(int x) { return x + 1; } std::array<int, f(5)> array1; // ok. f(5) is evaluated in compile time int x = 5; atd::array<int, f(x)> array2; // error. f(x) works as a normal function // the value is evaluated int runtime ``` 1. function的param type和return type都要是literal type,若type是class type,其base class不能是virtual 2. function會是inline 3. function不能是virtual 4. function body不能有`goto`和`try` 5. template的全特化定義也可以是`constexpr` 6. 若一個template是`constexpr`,其全特化定義不一定要是`constexpr` 7. body中宣告的變數必須要是literal type,且不能有`static`、`thread_local` 在上述的第一點中,literal type可以是 1. (C++14以後)`void` type 2. scalar type 3. reference type 4. array of literal type 5. class type * dtor是trivial * ctor為`constexpr`,或者使用aggregate方式初始化 C++14中,允許`constexpr` member function不是`const`,也允許literal type是`void` 因此以下寫法中,`setX`和`setY`都可以是`constexpr`,這讓setter可以在其他`constexpr`中被使用 ```cpp= class Point { public: constexpr Point(int x, int y): x(x), y(y) {} constexpr int getX() const { return x; } constexpr int getY() const { return y; } constexpr void setX(int val) { x = val; } constexpr void setY(int val) { y = val; } private: int x; int y; }; constexpr reflection(const Point& p) { Point result; result.setX(-p.getX()); result.setY(-p.getY()); return result; } ``` 要注意`constexpr` function可以被應用在其他`constexpr` function或variable中,因此應將其視為interface的一部份,如同`public`、`private`一樣 一旦宣告其為`constexpr`,使用者便有可能將其應用在`constexpr`中,若後來反悔移除`constexpr`,便有可能導致使用者編譯失敗 ## Item 16: Make `const` member functions thread safe user可能會在multi-thread的狀況下呼叫`const` member function,這是因為`const`語意保證不修改member data 但某些狀況下,我們可能還是需要在`const` function中修改member data 例如cache機制,或是要access被critical section保護的data ```cpp= class Polynomial { public: using RootsType = std::vector<double>; RootsType roots() const { if (!rootsAreValid) { // calculate the roots // ... rootsAreValid = true; } return rootVals; } private: mutable bool rootsAreValid { false }; mutable RootsType rootVals {}; }; ``` 上例中`Polynomial`是用來求多項式的解的class,`roots()` function是用來取得解的function 由於求解不會影響多項式怎麼寫,其為`const`function 但由於求解可能會花很多時間,若每次user呼叫`roots()`都要計算一次,很花時間 因此實作時用cache,只在第一次呼叫`roots()`時計算 `rootsAreValid`和`rootVals`都是`mutable`,因此可以在`const` function中修改 這實作在multi-thread時不work,因為沒有做同步機制 但對user來說,在multi-thread環境下呼叫`const`不應有任何影響 因此`roots()`應該要用lock保護 ```cpp= class Polynomial { public: using RootsType = std::vector<double>; RootsType roots() const { std::lock_guard<std::mutex> g(m); if (!rootsAreValid) { // .... rootsAreValid = true; } return rootVals; } private: mutable std::mutex m; mutable bool rootsAreValid { false }; mutable RootsType rootVals {}; }; ``` 同樣狀況也適用在需要在`const` function中使用counter的場合,此時可以用`std::atomic` ```cpp= class Point { public: double distanceFromOrigin() const { ++callCount; return std::sqrt((x * x) + (y * y)); } private: mutable std::atomic<unsigned> callCount { 0 }; double x, y; }; ``` 請注意由於`std::mutex`和`std::atomic`都不能copy,只能move,因此加入這兩個member會讓class變成無法copy ## Item 17: Understand special member function generation special member functions: 在C++11之前,指的是default ctor、dtor、copy ctor和copy assignment operator 這些function compiler會視情況自動產生 C++11之後,也包含move ctor和move assignment operator 自動生成的function都是`public`且`inline` 生成規則: * 若有自定義copy ctor / copy assignment,則不會自動生成move ctor / move assignment,反過來也成立,拒絕生成的function會用`delete`標記 * 若move ctor和move assignment只有其中一個有被自定義,另一個會被標記為`delete`,即兩者有連帶關係,要馬一起自定義,要馬都由compiler自動生成 * 若有自定義dtor,會拒絕生成move ctor / move assignment,標記為`delete` * 若有自定義copy ctor / dtor,自動生成copy assignment的行為是deprecated,可能再往後版本會移除 * 若有自定義copy assignment / dtor,自動生成copy ctor的行為是deprecated 生成的move ctor / move assignment會逐一對member data做move請求,若該member data支援move ctor/assignment,就會move,否則會變成copy ([Item 23](#Item-23-Understand-std::move-and-std::forward)) 即使compiler可以自動產生相關的member function,且其行為與預期符合,依然可以用`=default`來自行宣告,也可避免以下情況 ```cpp= class StringTable { public: StringTable() { ... } // no other special member functions }; ``` 可能在一開始時,`StringTable`只宣告了constructor,其他special member function都沒有,程式可以正常運作,用到copy,move等操作也都OK,compiler會自動產生對應程式碼 但在後來,由於偶些原因,決定要加入destructor,狀況就會有不同 ```cpp= class StringTable { public: StringTable() { ... } ~StringTable() { ... } // no other special member functions }; ``` 雖然程式可能還是可以正常運行,但實際行為有變 由於有自定義的destructor,compiler不再自動產生move相關的member function,因此本來預期會用到move的操作,都將被轉成copy,有可能導致效能上的差異 # Chapter 4. Smart Pointers ## Item 18: Use std::unique_ptr for exclusive-ownership resource management `std::unique_ptr`不能被copy,只能被move,適合用到同時間只能有一個owner的狀況 如factory function,通常會在heap上配置空間,再把物件回傳給caller,ownership被轉交給caller 或者把物件加入container時,要把ownership也一併交給container時 `std::unique_ptr`可以隱性轉換成`std::shared_ptr`,因此如factory function這種function,可以只單純回傳`std::unique_ptr`,而不用煩惱caller要如何處理 ```cpp= // classes class Investment { ... }; class Stock : public Investment { ... }; class Bound : public Investment { ... }; class RealEstate : public Investment { ... }; // factory function template<typename... Ts> std::unique_ptr<Investment> makeInvestment(Ts&&... params) { ... } // caller { ... auto ptr = makeInvestment(args); // unique_ptr to Investment ... } // leave scope, ptr is deleted ``` `std::unique_ptr`允許使用自定義deleter,當無指定時,預設會使用`delete`刪除物件 ```cpp= // customized deleter auto delInvmt = [](Investment* ptr) { makeLogEntry(ptr); delete ptr; }; // C++11 template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); if (...) { // cannot directly assign raw pointer to std::unique_ptr // must use reset() pInv.reset(new Stock(std::forward<Ts>(params)...)); } else if (...) { pInv.reset(new Bond(std::forward<Ts>(params)...)); } else if (...) { pInv.reset(new RealEstate(std::forward<Ts>(params)...)); } return pInv; } // C++14 auto return type template<typenme... Ts> auto makeInvestment(Ts&&... params) { // Move deleter inside the function auto delInvmt = [](Investment* ptr) { makeLogEntry(ptr); delete ptr; }; std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // remains are the same as C++11 version ... return pInv; } ``` 1. 自定義的deleter要放在`std::unique_ptr`的第二個template parameter中 2. deleter可以是function pointer,function object,或是lambda function 3. 自定義deleter必須接收一個pointer to type,沒有回傳值 4. 當有自定義deleter時,`std::unique_ptr`的大小視deleter大小而定 沒有deleter時,大小與raw pointer相同 deleter是function pointer,會多一個pointer的大小 若是無狀態function object (如無擷取的lambda),則大小與raw pointer相同 5. raw pointer不能直接assign給`std::unique_ptr`,必須使用`reset()` 6. `std::forward`使用是搭配universal reference,詳見 [Item 25](#Item-25-Use-std::move-on-rvalue-references-std::forward-on-universal-references) 7. deleter的type會成為`std::unique_ptr`的一部份 ```cpp= // del1 and del2 have different types auto del1 = [](Widget* p) { ... }; auto del2 = [](Widget* p) { ... }; // p1 and p2 have different types as well std::unique_ptr<Widget, decltype(del1)> p1; std::unique_ptr<Widget, decltype(del2)> p2; ``` ## Item 19: Use std::shared_ptr for shared-ownership resourfe management `std::shared_ptr`使用在需要共享資源的狀況,內部會用reference counter紀錄 1. `std::shared_ptr`的大小通常是raw pointer的兩倍大 2. 當`std::shared_ptr`的ctor被呼叫時(move ctor除外),reference count增加 3. 使用move ctor時,舊的`std::shared_ptr`會被設成null,而新的`std::shared_ptr`會持有原本的物件,因此reference count不會增減, 4. 當dtor被呼叫時,reference count減少,至0時物件被刪除 5. Copy assignment可能會各有增減 如`sp1 = sp2`,`sp1`所使用的reference count會減少,assign後,`sp2`所使用的reference count會增加 6. `std::shared_ptr`的大小通常是raw pointer的兩倍大,此是因為有reference counter的緣故 7. reference counter的增減是atomic操作,這會增加操作的時間 8. reference counter的空間是動態配置出來的,若不是用`std::make_shared` ([Item 21](#Item-21-Prefer-std::make_unique-and-std::make_shared-to-direct-use-of-new)),生成`std::shared_ptr`時需要額外成本動態配置reference counter `std::shared_ptr`同樣可以自定義deleter,當不指定時,預設同樣為`delete` `std::unique_ptr`的自定義deleter是type的一部份,`std::shared_ptr`則不是,其為ctor的一部份 ```cpp= // del1 and del2 have different types auto del1 = [](Widget* p) { ... }; auto del2 = [](Widget* p) { ... }; // p1 and p2 have the same type std::shared_ptr<Widget> p1(new Widget, del1); std::shared_ptr<Widget> p2(new Widget, del2); // ok std::vector<std::shared_ptr<Widget>> v { p1, p2 }; ``` 為`std::shared_ptr`自定義deleter不會讓`std::shared_ptr`物件增加size `std::shared_ptr`的size總是兩個pointer的大小,其實作如下 ![](https://i.imgur.com/qMKkzuk.png) Control Block保存`std::shared_ptr`所需的metadata,因此所有額外資訊皆在heap,而不是物件之中 這與`std::unique_ptr`不同,`std::unique_ptr`將資訊存在物件本身 Control Block的建立時機是第一次create一個指向物件的`std::shared_ptr`時,如下: 1. 使用`std::make_shared`([Item 21](#Item-21-Prefer-std::make_unique-and-std::make_shared-to-direct-use-of-new))建立`std::shared_ptr`時,一定會建立Control Block 2. 從`std::unique_ptr`建立`std::shared_ptr`時。此時`std::shared_ptr`會取得ownership,因此`std::unique_ptr`會被設定成null 3. 從raw pointer建立`std::shared_ptr`時 4. 從`std::weak_ptr`([Item 20](#Item-20-Use-std::weak_ptr-for-std::shared_ptr-like-pointers-that-can-dangle))或`std::shared_ptr`建立時,並不會產生新的Control Block 從第3點得知,若使用同一個raw pointer去產生多個`std::shared_ptr`,會產生多個Control Block 這會導致raw pointer被delete多次 因此,不應使用已存在的raw pointer,去初始化一個`std::shared_ptr` 而應使用`std::make_shared` (當不指定deleter時),或是將create raw pointer動作inline在`std::shared_ptr`的ctor中 ```cpp= // not recommend { auto p = new Widget; std::shared_ptr<Widget> sp1(p); // create a control block std::shared_ptr<Widget> sp2(p); // create another control block } // end of the block, p will be deleted twice causing UB // use std::make_shared when no customized deleter { std::shared_ptr<Widget> sp1 = std::make_shared<Widget>(); std::shared_ptr<Widget> sp2(sp1); // sp2 uses same control block as sp1 } // new object inline to ctor of std:shared_ptr { std::shared_ptr<Widget> sp1(new Widget, customizedDel); std::shared_ptr<Widget> sp2(sp1); // sp2 uses same control block as sp1 } ``` 另一種從raw pointer建立`std::shared_ptr`會產生問題的情境,是用`this`pointer產生 ```cpp= // record which widgets we already processed std::vector<std::shared_ptr<Widget>> processedWidget; class Widget { public: ... void process() { ... // this pointer is already processed, add it into processedWidget // This will create a control block processWidgets.emplace_back(this); } }; std::shared_ptr<Widget> sp(new Widget); // create a control block sp.process(); // UB. Widget::process() will create another control block ``` STL提供`std::enable_shared_from_this`來解決此問題 ```cpp= // record which widgets we already processed std::vector<std::shared_ptr<Widget>> processedWidget; class Widget : public std::enable_shared_from_this<Widget> { public: ... void process() { ... // this pointer is already processed, add it into processedWidget // Call shared_from_this to generate a shared_ptr // shared_from_this() won't create new control block // It uses the existed one processWidgets.emplace_back(shared_from_this()); } }; std::shared_ptr<Widget> sp(new Widget); // create a control block sp.process(); // OK. Widget::process() no longer creates new control block // It uses sp's control block ``` 1. `std::enable_shared_from_this`是base class template,其參數型態必須是繼承`std::enable_shared_from_this`的型態(`Widget`),此種用法為Curiously Recurring Template Pattern (CRTP). 2. `std::enable_shared_from_this`定義一個function`shared_from_this`,此function可以被使用在member function中,去create一個`std::shared_ptr`指向`this`,但不建立Control Block,該`std::shared_ptr`的Control Block會使用擁有`this`的`std::shared_ptr`的control block。例子中即為`sp` 3. 呼叫`shared_from_this()`時,Control Block一定要存在,否則造成UB或是拋出例外。因此`shared_from_this()`不能在ctor與dtor中呼叫 4. 同3,因此繼承`std::enable_shared_from_this`的class,通常讓ctor變成`private`,並提供factory function回傳`std::shared_ptr`,以確保該class必定在`std::shared_ptr`的控制之下 ```cpp= class Widget : public std::enable_shared_from_this<Widget> { public: // factory function to create Widget // This make Widget class controled by std::shared_ptr template<typename... Ts> static std::shared_ptr<Widget> create(Ts&&... params); // functions that calls shared_from_this() void process(); private: Widget(...); }; ``` ### NOTE: Curiously Recurring Template Pattern (CRTP) CRTP是c++ template programming的一種pattern,此pattern會把derived class的名稱,作為base template class的template paramter使用 也稱為靜態多型 (static polymorphism),是因為`Base`可以透過template paramter去存取Derived的members ```cpp= // Base assume that T is a class which derives Base // Base can utilize T to do something template<typename T> struct Base { // 'interface' is just like a virtual function // And Derived implementation the virtual function as 'implementation' void interface() { static_cast<T*>(this)->implementation(); } // static function can work as well static void static_func() { T::static_sub_func(); } }; struct Derived : public Base<Derived> { void implementation() { // ... } static void static_sub_func() { // ... } }; Derived d; // Call Based<Derived>::interface() -> Derived::implementation() d.interface(); // Call Based<Derived>::static_func() -> Derived::static_sub_func() Base<Derived>::static_func(); ``` - 不使用CRTP時,若一個base class沒有virtual function,則該base的所有member function只能夠呼叫到base的member function - 若一個derived class繼承base,derived繼承所有沒有被overridden的base member function,derived也可以呼叫base的member function,但一旦呼叫了,其calling sequence將不會回到derived手上 (因為base沒有virtual) - 使用CRTP,base可以透過template paramter`T`去存取derived的member,因此可以達到類似virtual function的效果,derived呼叫base member function,calling sequence也有可能會回到derived手上 - 上述例子的缺點是`base`不能存取`derived`的`private`與`protected`,若`Derived::implementation`不是`public`,編譯會失敗 - 此問題可以透過將`Base<Derived>`宣告為friend class解決 - 也可以透過accessor class解決,但此方式僅能存取Derived的`public`和`protected` member ```cpp= // Friend class version template<typename T> class Base { public: void interface() { static_cast<T*>(this)->implementation(); } static void static_func() { T::static_sub_func(); } }; class Derived : public Base<Derived> { // Resolve by declare Base<Derived> to a friend class friend class Base<Derived>; void implementation() { // ... } static void static_sub_func() { // ... } }; ``` ```cpp= template<typename T> class Base { // accessor inherit T and can get public & protected functions from T // Use derived (converted by Base<Derived>) to call the function struct accessor : T { static void interface(T& derived) { void (T::*fn)() = &accessor::implementation; (derived.*fn)(); } }; // Utility function to return derived type of this T& derived() { return static_cast<T&>(*this); } public: void interface() { // delegate to accessor return accessor::interface(this->derived()); } // static function won't work by accessor // static void static_func() // { // T::static_sub_func(); // } }; class Derived : public Base<Derived> { void implementation() { // ... } }; ``` #### example 1: 用相同interface,讓derived type回傳不同型態的value ```cpp= // C++ 14 only template<typename T> struct Base { auto interface() { return static_cast<T*>(this)->impl(); } }; struct Derived1 : public Base<Derived1> { int impl() { return 1; } }; struct Derived2 : public Base<Derived1> { double impl() { return 3.14; } }; Derived1 d1; Derived2 d2; auto v1 = d1.interface(); // v1 is int auto v2 = d2.interface(); // v2 is double ``` #### example 2: object counter 若要使用counter去統計 TODO ## Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle `std::weak_ptr`無法單獨存在,使用必須配合`std::shared_ptr` `std::weak_ptr`代表他可以使用`std::shared_ptr`指向的resource,但卻不擁有該resource (或稱weak reference) 當用`std::shared_ptr`去建立`std::weak_ptr`時,reference count不會增加 `std::weak_ptr`不能dereference,每當要用`std::weak_ptr`存取resource時,必須轉型成`std::shared_ptr`,此時`std::weak_ptr`會暫時擁有resource的所有權 由於`std::weak_ptr`並不是真正擁有所有權,有可能在想要存取時,該resource已經消失,`std::weak_ptr`提供function來確認resource是否依然存在,或者已經消失 (dangle) ```cpp= auto sp = std::make_shared<Widget>(); // ref count = 1 std::weak_ptr<Widget> wp(sp); // ref count = 1 if (wp.expired()) // expired() check is the resource is deleted { ... } // convert weak_ptr to shared_ptr std::shared_ptr<Widget> sp2 = wp.lock(); // ref count = 2 // convert weak_ptr to shared_ptr std::shared_ptr<Widget> sp3(wp); // ref count = 3 ``` 1. `expired()`function可以用來確認resource是否已經消失 2. `std::weak_ptr`不能直接dereference,用`lock()`來轉型成`std::shared_ptr`,若轉型時該resource已經消失,則轉成的pointer會是null 3. 同2,也能用`std::shared_ptr`的ctor來把`std::weak_ptr`轉型,若轉型時resource已消失,則會丟出例外`std::bad_weak_ptr` 4. 依據`std::weak_ptr`語意,當轉型成`std::shared_ptr`,只是暫時擁有resource (或理解為暫時借用),因此轉型後的`std::shared_ptr`的生命週期應該盡量的短,不應該長時間持有 5. `std::weak_ptr`的使用效率,與`std::shared_ptr`相當 ### 使用時機 1. 具有cache功能的factory function - 假設有一factory function `loadWidget(WidgetId)`,回傳一個smart pointer,根據其語意,生成的widget必須由caller來控管 - factory function不應擁有resource的ownership (ownership應由caller所持有) - 由於生成widget成本很高,因此想藉由cache來加快速度 - 若cache是valid (caller還持有resource),則回傳cache中的resource - 若cache是invalid (caller已釋放resource),Widget 此情境中,回傳`std::unique_ptr`並不適合,這是因為雖然Caller確實需要接收smart pointer,也確實要控管該resource的生命週期 但是cache也需要確認該resource是否已被銷毀,`std::unique_ptr`無法做到這點 `std::weak_ptr`可以就可以做到不持有ownership,但又可以確認resource是否已被銷毀的功能 ```cpp= std::shared_ptr<const Widget> loadWidget(WidgetId id) { // Cache is a weak_ptr, which means we don't own the resource // We just want to check if the cache is valid or not static std::unordered_map<WidgetId, std::weak_ptr<const Widget> cache; // ptr is a shared_ptr of a cached resource // If the resource is not in cache, ptr = null auto ptr = cache[id].lock(); if (!ptr) { // cache is not valid. There is no caller own the resource // Need to create a new one ptr = createWidget(id); cache[id] = ptr; } return ptr; } ``` ### 使用時機 2. Observer pattern Observer pattern中,subject表示目標,其狀態的變化可能會需要讓其他人知道 observer表示觀察者,向subject訂閱,表示該observer對subject感興趣,若subject發生變化時,主動通知observer ```cpp= // Observer and Subject are interfaces class Observer; class Subject; class Subject { public: virtual void register(Observer*) = 0; private: virtual void notify() = 0; }; class Observer { public: virtual onNotified() = 0; }; // concrete subject class ConcreteSubject : public Subject { public: void register(Observer* observer) override { observers.push_back(observer); } private: void notify() override { for (auto& o : observers) { o->onNotified(); } } private: std::vector<Observer*> observers; }; // concrete observer class ConcreteObserver : public Observer { public: void onNotified() override { ... } }; ``` 通常subject會需要維護一個資料結構來保存註冊的observer subject並不擁有observer,但是卻需要確認observer是否還存在 若使用到已被釋放的observer,就會引發問題 此狀況可用`std::weak_ptr` ```cpp= class ConcreteSubject : public Subject { public: void register(std::weak_ptr<Observer> observer) override { observers.push_back(observer); } private: void notify() override { for (auto& o : observers) { auto p = o.lock(); if (p) { p->onNotified(); } } } private: // subject does not own observers // but only wants to check if they are still alive std::vector<std::weak_ptr<Observer>> observers; }; ``` ### 使用時機 3. cycle reference 假設有以下資料結構,包含Class A, B 和 C,A和C各有一個`std::shared_ptr`to B,而B需要一個指向A的pointer 該pointer應該為何? ![](https://i.imgur.com/BJzeodD.png) - raw pointer: 當A被銷毀,但C依然存在時,B也會存在 (ref count = 1),因此B還是可以存取A,但由於透過raw pointer存取時無法確認A是否還存在,直接存取會導致UB - `std::shared_ptr`: 此將導致循環引用,A和B互相擁有對方的`std::shared_ptr`,ref count永遠不會是0,將導致leak - `std::weak_ptr`: 可以避免上兩種狀況遇到的問題,既可以確認A是否還存在,也可以避免循環引用 ## Item 21: Prefer std::make_unique and std::make_shared to direct use of new `make`function有三種 1. `std::make_shared`在C++ 11中加入 2. `std::allocate_shared`,除了第一個參數是一個Allocator的物件外,和`make_shared`類似 3. `std::make_unique`在C++ 14中加入,C++ 11可用以下實作替代 ```cpp= // Implement std::make_unique in C++11 template<typename T, typename... Ts> std::unique<T> make_unique(Ts&&... params) { return std::unique<T>(new T(std::forward<Ts>(params)...)); } ``` ### `make`function的好處 1. 可以減少不必要的type指名 當使用`make_unique`和`make_shared`時,只需要把type寫為`make`function的template parameter即可 若不使用時,則在`new`以及pointer的type都需要指名type ```cpp= // unique_ptr auto up1(std::make_unique<Widget>()); // use make function std::unique_ptr<Widget> up2(new Widget()); // not use make function auto up3 = std::unique_ptr<Widget>(new Widget()); // same above // shared_ptr auto sp1(std::make_shared<Widget>()); // use make function std::shared_ptr<Widget> sp2(new Widget()); // not use make function auto sp3 = std::shared_ptr<Widget>(new Widget()); // same above ``` 2. 可以避免memory leak 若不使用`make`function,以下程式碼可能會有leak ```cpp= // function to process widget according to the priority void processWidget(std::shared_ptr<Widget> w, int priority); // compute the priority, this function may throw exception int computePriority(); // may leak Widget* processWidget(std::shared_ptr<Widget>(new Widget()), computePriority()); ``` 在執行`processWidget`前,compiler需要先evaluate每個參數的值 原則是只要`new`發生在`shared_ptr`的ctor前即可,其他並無順序限制 因此evaluate順序可能會是以下 a. `new Widget()` b. `computePriority()` c. `std::shared_ptr<Widget>`的ctor 此順序下,若`computePriority`丟出exception,由於`shared_ptr`還沒有被產生,`new Widget`就會leak 使用`make`function時,則不會有這樣的狀況產生 然而,若某些時候無法使用`make_shared`時,還是得直接用`new`,此狀況應該把`std::shared_ptr`的ctor獨立出來,才能避免leak ```cpp= void processWidget(std::shared_ptr<Widget>, int); // myDel can still be called even when shared_ptr ctor throws exception std::shared_ptr<Widget> sp(new Widget, myDel); processWidget(sp, computePriority()); // correct, but not good enough ``` 此例中即使`sp`的constructor拋出例外,自定義的deleter依然會被呼叫 若`computePriority`丟出例外,被`std::shared_ptr`控管的指標也會被刪除 與原本`processWidget(new Widget, ...)`相比可能有細微的效能差異 在於本來的`processWidget(new Widget, ...)`傳入的是rvalue 而新版本`processWidget(sp, ...)`傳入lvalue 傳入lvalue時,會做copy,此時`shared_ptr`會需要增加ref count,增加成本 可明確用`std::move`,此時成本就會一樣 ```cpp= void processWidget(std::shared_ptr<Widget>, int); // myDel can still be called even when shared_ptr ctor throws exception std::shared_ptr<Widget> sp(new Widget, myDel); processWidget(std::move(sp), computePriority()); ``` #### NOTE: [Copy elision](https://en.cppreference.com/w/cpp/language/copy_elision) Compiler在某些狀況下,可能會忽略copy / move,此稱為copy/move elision `processWidget`第一版本的呼叫狀況(會leak版本),第一個參數是pass by value,物件本身應當被copy 但若呼叫端傳入的是個temporary object,由於該object不會被其他地方使用,compiler可以做copy elision,直接把該物件配置在callee的stack上,增進performance 此在C++ 14不是必須,但在C++ 17後為必須 3. `make_shared`可以增進效率 若用`std::shared_ptr<T>(new T())`,會發生兩次heap allocation,一次是`new T`,一次是配置`shared_ptr`control block 若用`std::make_shared`,STL會把control block和`T`所需的空間一次配置好 `T`object是in-place的成為control block的data member ### 無法使用`make`function的時機 1. 需要自定義deleter時 `make`function不提供自定義deleter,因此若想create一個用自定義deleter的smart pointer時,還是要用`new` 2. 當想使用`std::initializer_list`的ctor來建立物件時 `make`function使用小括號做perfect forwarding,因此以下狀況,會產生一個包含10個數值為20的`vector` ```cpp= // make function use () rather than {} to forward arguments auto up = std::make_unique<std::vector<int>>(10, 20); auto sp = std::make_shared<std::vector<int>>(10, 20); ``` 根據[Item 30](#Item-30-Familiarize-yourself-with-perfect-forwarding-failure-cases),`std::initializer_list`無法perfect forwarding,因此`make`function無法接受大括號的初始化方式 ```cpp= // error. Cannot compile the following code // auto sp = std::make_shared<std::vector<int>>({ 10, 20 }); // workaround auto initList = { 10, 20 }; // create std::initializer_list // create std::vector using std::initializer_list ctor auto sp = std::make_shared<std::vector<int>>(initList); ``` 3. 若class有自定義的`operator new`或`operator delete`,`make_shared`不適用 這是因為`make_shared`會一同將control block和object產生,object的大小被包含在control block中 因此在`make_shared`中,會使用placement new去初始化object,而因此客製化的`operator new`將不會被使用 同理在delete時,只是呼叫dtor,而自定義的`operator delete`不會被呼叫 另外,`make_shared`產生的object,由於生命週期與control block同在,只有當所有的refcount=0時,才會被delete 若shared refcount=0,但weak refcount!=0,則control block還是存在,因此object還不會被delete 因此若物件非常大,這樣被延後delete的行為,在使用`make_shared`可能會有極大影響 ```cpp= class ReallyBigType { ... }; // construct an object which is very big via make_shared // shared ref = 1 auto sp1 = std::make_shared<ReallyBigType>(); ... // construct a weak_ptr // weak ref = 1 std::weak_ptr<ReallyBigType> wp(sp1); // sp2 shared the same object // shared ref = 2 auto sp2 = sp1; sp2.reset(); // shared ref = 1 sp1.reset(); // shared ref = 0 // The object is not deleted yet because weak ref = 1 // weak ref = 0, the object is deleted wp.reset(); ``` 若是直接使用`new`,則當`sp1.reset()`時,object就會被delete,只剩下control block還留著 ```cpp= class ReallyBigType { ... }; // construct an object which is very big via make_shared // shared ref = 1 auto sp1 = std::shared_ptr<ReallyBigType>(new ReallyBigType()); ... // construct a weak_ptr // weak ref = 1 std::weak_ptr<ReallyBigType> wp(sp1); // sp2 shared the same object // shared ref = 2 auto sp2 = sp1; sp2.reset(); // shared ref = 1 sp1.reset(); // shared ref = 0, the object is deleted // The control block is not deleted yet because weak ref = 1 // weak ref = 0, the control block is deleted wp.reset(); ``` ## Item 22: When using the Pimpl Idiom, define special member functions in the implementation file Pimpl idiom是把實作與介面分開的手法,藉由forward declaration,只將介面放在header file,使用者可以在不關心實作的情況下使用,實作修改時,使用者不用重新編譯 ```cpp= // Widget.h class Widget { public: Widget(); ~Widget(); private: struct Impl; // foward declaration Impl* pImpl; }; // Widget.cpp #include "Widget.h" // include dependence #include <string> ... struct Widget::Impl { std::string name; ... }; Widget::Widget() : pImpl(new Impl) { } Widget::~Widget() { delete pImpl; } ``` 由於`pImpl`僅`Widget`使用,此情境適用`std::unique_ptr` ```cpp= // Widget.h class Widget { public: Widget(); // ~Widget(); // no need dtor because we use unique_ptr? private: struct Impl; // foward declaration std::unique<Impl> pImpl; }; // Widget.cpp #include "Widget.h" ... struct Impl { ... }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } // Widget::~Widget() {} ``` 在使用`unique_ptr`後,如果把dtor拿掉 (由於把動態配置交給smart pointer),則在使用時會出錯 其原因通常是delete on incomplete type ```cpp= // Client.cpp #include "Widget.h" { Widget w; } // leave scope, error on delete w ``` 在`Widget.h`中,`unique_ptr`是可以放入incomplete type的 但是由於在`Client.cpp`中,物件刪除時,會呼叫dtor `Widget.h`沒有宣告自定義dtor時,將使用compiler產生的dtor,該dtor為inline,內容是呼叫`unique_ptr`的dtor `unique_ptr`的dtor預設會呼叫`delete`on object,而`delete`不應刪除incomplete type的object 這錯誤基於呼叫dtor的情境下,`Impl`是incomplete type 要解決此錯誤,只要讓`Widget::~Widget`不要inline就可以了 ```cpp= // Widget.h class Widget { public: Widget(); ~Widget(); // we actually need this private: struct Impl; // foward declaration std::unique<Impl> pImpl; }; // Widget.cpp #include "Widget.h" ... struct Impl { ... }; Widget::Widget() : pImpl(std::make_unique<Impl>()) { } // define dtor inside the cpp file Widget::~Widget() {} // The following is also ok Widget::~Widget() = default; ``` 其他相關的special member function如move ctor等也適用同樣的方法 相反的,`std::shared_ptr`則不需要 這體現於`std::shared_ptr`與`std::unique_ptr`對於deleter實作的不同 由於deleter是`std::unique_ptr`型態的一部份,當用default deleter宣告`std::unique_ptr`時,deleter就已經決定為`std::default_delete` (header file中決定) 但`std::shared_ptr`不會,由於deleter是ctor的一部份而不是type,僅宣告`std::shared_ptr`並不會決定deleter 決定時機是在用raw pointer呼叫ctor或是`reset`時候 在pimpl中,這時機會在cpp file中,此時`Impl`已是complete type,因此沒有問題 ---