# effective c++ 筆記 ## 讓 const 函式保持 thread safe 如標題,這是個好習慣 ## 成員有 mutex 會讓 obj 只能搬移無法複製 因為 mutex 本身只能搬移,不給複製。 嘗試了一下 ```cpp #include <iostream> #include <mutex> using namespace std; class test { mutex m; }; int main() { test a; auto b = a; return 0; } ``` 結果 ``` ..\Playground\:5:7: error: use of deleted function 'std::mutex::mutex(const std::mutex&)' In file included from ..\Playground\-w64..\Playground\-w64-mingw32..\Playground\++/mutex:43:0, from ..\Playground\:2: ..\Playground\-w64..\Playground\-w64-mingw32..\Playground\++/bits/std_mutex.h:97:5: note: declared here mutex(const mutex&) = delete; ^~~~~ ``` 真的不行耶好帥喔XDDD ## 盡可能使用 constexpr 這會讓編譯器盡可能把這個 function 或是 object 在編譯時期就做完。 就算是 function 也會嘗試帶入並且在編譯時期完成計算並展開 注意內部不能有 IO 函式,IO 不可能在編譯時期做完。 ## 不會產生例外的函式宣告為 noexcept 對於 noexcept 函式,執行時不用把 call stack 保持在可以修復的狀態(正常的要,如果外面有 try catch 的話程式必須要能修復) 炸了直接噴出 callstack 然後離開就是。 這部份可以最佳化 此外只有 noexcept 的操作才能搬移(如 move 或 swap) 建構子跟解構子天生就是 noexcept,這是 C++ 規範的一部分 ## 了解特殊成員函式的生成 預設建構子 解構子 複製建構子 複製賦值運算子 搬移建構子 搬移賦職運算子 如果啥都沒宣告,會全部自動生成 宣告了複製就不會生成賦值 宣告了賦值就不會生成複製 宣告了解構的話就會只有複製 明確一點還是自己宣告出來加上 default 讓編譯器生成會更好 ## std::unique_ptr 不允許複製,只允許搬移。 單一持有權。 可在創建時傳入自訂解構子,自訂解構子會成為該型別的一部分 無法從原始指標自動轉型成 unique_ptr -> 不能把 new 出來的直接給 unique_ptr (因為程式沒辦法確認你有沒有把 new 出來的記憶體空間傳給兩個 unique_ptr,所以乾脆禁止這種操作) 遵照只允許搬移的原則, new 出來的請用 std::forward 傳給 unique_ptr 自訂解構子會讓 unique_ptr 的空間增大,尤其是內部狀態資訊很多的物件,這時可以指名無狀態來減少這些空間消耗。 用無擷取的 lambda 當作解構子可以作到上面的需求。 unique_ptr 也常用在 pimpl Idiom 的實作。 可以輕鬆轉換成 share_ptr 這個是在界面設計上的考量,一個物件回傳時可能會不知道是否具有分享性,先寫成 unique_ptr 讓使用者可以自行決定要不要轉型成 share_ptr 此外也支援陣列型的 unique_ptr,不過是沒有必要的存在,沒事不要用。 ## std::shared_ptr shared_ptr 賦值或複製會增加計數,搬移不增加,解構減少。 歸零時呼叫原本的解構子。 大小是原始指標的兩倍,內部包含指向計數的指標。 std::make_shared 可以避免一些動態配置的負擔。 > 原理參考 https://blog.csdn.net/love_hot_girl/article/details/21161507 計數是 atomic 的,代表在多線程讀寫時效能較差。 搬移不修改計數,所以效能較好。 也可以指定自訂解構子,自訂解構子不屬於 shared_ptr 的一部分。 自訂解構子存在控制區塊(也就是 shared_ptr 內的第二個指標指向的區域) 會不會建立控制區塊取決於建立 shared_ptr 的方式,因為不同 shared_ptr 之間沒辦法知道是否已經有其他人建立過控制區塊,所以用建立方式判斷,以下幾種方式會建立控制區塊。 * make_shared * 從單一所有權轉移到 shared_ptr(unique_ptr) * 從裸指針建立 > Q : 針對某個裸指針建立兩次 shared_ptr 的話會怎樣 > A : 會炸掉,因為不同的控制區塊指向同一個指標位置,會發生重複釋放。 > ```C++ int main() { auto test = new foo; shared_ptr<foo> a{test}; test -> test = 2; { shared_ptr<foo> b{test}; } cout << a->test <<endl; return 0; } // print: // break // 3871440 // break ``` 這種狀況無法應用在 make_shared 上,因為 make_shared 本身就是一個客製化的建構子,是沒辦法取得其裸指針的(硬要做應該也是可以,但這樣太低能了我不想嘗試XD) > 笑死我發現下一頁就在講這個問題XD 所以應該盡量用 make_shared 或是直接把 new 放在 shared_ptr 的建構子內就不會有這問題。 如果要傳入 this 請用 std::enable_shared_from_this 繼承這個類別就像 make_shared 的作法一樣,會直接把控制區塊嵌在物件內部,這樣取 shared_from_this() 的時候也保證所有人都可以取得一樣控制區塊。 通常繼承之後還會把建構子設為 private ,然後新增一個 create 方法,把建構子包在裡面,然後只回傳已經包成 shared_ptr 的物件,來確保所有建立出來的此物件都是建立在 shared_ptr 上。 此外 unique_ptr 到 shared_ptr 是單向的,轉過去就轉不回來了。 shared_ptr 不支援陣列操作,有需要就自己丟進 vector 裡面操作。 ## 用 std::weak_ptr 取代可能懸置的 std::shared_ptr weak_ptr 是不增加索引的 shared_ptr 雖然有 expire 函式可以檢查,但檢查加取得 shared_ptr 本身依然得是 atomic 操作 所以有了 `std::weak_ptr::lock` ,如果取得時 shared_ptr 已經過期,則取得的會是 null 另一個作法是用 weak_ptr 作為 shared_ptr 的建構子,過期時會噴出 exception std::weak_ptr 通常用在快取,訂閱的功能上 以 Observer 模式來說,被訂閱者不用知道訂閱者的生命週期,只要把訊息發過去就是了。 但通知不存在的物件又有炸掉的風險,因此就可以用 weak_ptr 來處理這一塊。 循環引用的情況也只能用 std::shared_ptr,但通常設計出循環引用的正確案例很少,通常都是設計上有問題才會出現循環引用。 ## 盡量用 std::make_unique 和 std::make_shared 來取代 new make_unique 14 才有,不過自己寫一個也很簡單 ## rvalue 和 universal reference ```cpp void test(int&& A) { } ``` 這個代表 A 只能傳入為 rvalue 的 ```cpp template<typename T> void test (T&& A) { } ``` 這個代表 A 傳入的是 universal reference 對他們代表的意義不同,不是說樣板類型的 test 實作出來會變成 T 的 rvalue rvalue 只能接受 rvalue 傳入,但 universal reference 是 rvalue 跟 lvalue 都吃的。 所以在 rvalue 的部份要用 std::move ,而 universal reference 則一定要用 std::forward 不然把 lvalue 的參考做 move 會讓原本的值炸掉(因為這個值是有外部持有者的),還有可能導致越域存取(如果傳入的是區域變數) 補充一下 universal reference 其實可以單純被 "寫兩次代碼" 所取代,單純寫成 ```cpp void test(int&& A) { } void test(int& A) { } ``` 就可以做到一樣的效果,至於這樣做 ## std:move const object To make sure all the operation is safe compiler will call the "copy contructor" of that object, which may be quite slow and not the things that you want. ## 別嘗試對回傳的區域物件進行 move 這會妨礙 RVO 的運作,而且 C++ 有規範了。 ## 避免對 universal reference 進行 Overload universal reference 過載會導致呼叫他的機率暴高於預期, ## 最快的解決方案 傳入 const 就會自動變回複製建構子 或是單純傳入值就好,在沒必要的地方別堅持用搬移語意。 還有標籤分派 或用 std::enable_if 寫出一大串很靠腰的東西 ## reference collapse 樣板如果以參考來定義,在吃到參考展開時會變成參考的參考 這個時候編譯器會自動幫忙做壓縮 如果兩個參考都是 rvalue 才會套用 rvalue 的參考 某則就轉為 lvalue 的參考 ## 假設搬移操作不存在,就算存在也不一定比較快XDD 別妄想把 c++98 的東西放到 c++11 就會變快 幾種狀況搬移語意沒啥用 * 物件沒實作搬移操作,會自動轉為複製 * 搬移沒有比較快,類似 array or native object 或是短 string * 無法使用搬移,只有搬移操作保證 noexcept 才會真的使用搬移操作 * 來源是 const lvalue reference (std::move不動。它實際上將左值引用轉換為右值引用。在這種情況下,移動的結果是 a const A &&(順便說一句,這完全沒用)) 只有對確定型別也確定支援搬移的程式才做搬移語意。 ## Different between reference and pointer They are basicly the same. However, const reference may allow compiler do more optimization for the value (such as inline) ## 完美轉發的不完美之處 * 完美轉發是由樣板實作的,樣板被禁止推導型別 對於 `f(std::forward({1, 2, 3}));` 會掛掉,因為編譯器禁止 forward 的樣板推導 `{1, 2, 3}` 為初始列表 static const 成員也會炸,因為 static const 本質上就跟 #define 一樣,編譯器會把數值直接帶入該位置才編譯 但對於樣板來說帶入的數值沒有型態宣告,但又不能嘗試推導,就會爆炸。 對於樣板或是過載函式也母湯,編譯器會無法判斷到底是那一種的實作,但又被禁止推導。 bitfield 也不行,因為無法定址。