微調練習題 (Practice - Tweaks)
Related Concepts
| 關鍵字 | 答案 |
|---|---|
| 值傳遞 vs by-reference 成本 | 恆多一次 move(lvalue:1 copy + 1 move;rvalue:2 moves) |
| move-only 型別的 setter | 單一 rvalue reference overload;值傳遞把 1 move 變 2 moves |
| 參數以 assignment 複製 | 可能多付記憶體配置/釋放,成本遠超一次 move → 有罪推定 |
| base class 型別 + 值傳遞 | slicing problem:衍生類別特性被切掉 |
對值參數 std::move |
安全:獨立物件+最後一次使用 |
push_back("literal") |
暫時物件:2 建構 + 1 解構;emplace_back 僅 1 建構 |
| insertion vs emplacement 參數 | 收「物件」 vs 收「建構子引數」(perfect forwarding) |
| emplacement 幾乎必勝三條件 | 建構非指派、引數型別異於元素型別、不會被當重複值拒絕 |
emplace_back(nullptr) 到 regex 容器 |
可編譯(direct init 允許 explicit)但 UB |
emplace_back(new Widget, deleter) |
例外安全漏洞 → 先建 shared_ptr 再 push_backmove(spw) |
Question 1 - 值傳遞成本會計 [recall]
void addNamestring newName) { names.push_back(std::move(newName); }相較於 overloading 或 universal reference 方案,傳入 lvalue 與 rvalue 各付出多少 copy/move 成本?
值傳遞:lvalue → 1 copy + 1 move;rvalue → 2 moves。
By-reference 方案(overloading / universal reference):lvalue → 1 copy;rvalue → 1 move。
結論:值傳遞對 lvalue 與 rvalue 恆多一次 move——這正是「cheap to move」是前提條件的原因。
Question 2 - 標題四限定詞 [recall]
Item 41 的標題「Consider pass by value for copyable parameters that are cheap to move and always copied」中,四個限定詞各自防範什麼?
Consider:值傳遞恆有額外成本(且有 assignment、呼叫鏈等隱藏開銷),只能個案評估。
copyable:move-only 型別只需單一 rvalue reference overload,值傳遞反而把 1 move 變 2 moves。
cheap to move:多出的那次 move 若昂貴,等同做了不必要的 copy。
always copied:條件式複製時,不符合條件仍白付參數的建構+解構成本。
Question 3 - 對值參數 std::move [recall]
addNamestring newName函式體內對newName呼叫std::move—std::move不是通常用在 rvalue reference 上嗎?為什麼這裡安全?
安全的兩個理由:(1) newName 是與呼叫端引數完全獨立的物件,move 它不影響呼叫端;(2) 這是函式內最後一次使用 newName,之後不再讀取。
Question 4 - Slicing problem [recall]
void processWidget(Widget w)其中Widget是 base class,傳入SpecialWidget物件會發生什麼事?
Slicing problem:值傳遞只複製 base 部分,函式看到的是 Widget 而非 SpecialWidget,衍生類別的特性被「切掉」。
因此 base class 參數型別通常不適合值傳遞——這是效能之外,值傳遞在 C++98 名聲不佳的另一原因。
Question 5 - Move-only setter 設計 [application]
你要為
std::unique_ptr<std::string>成員寫一個 setter,同事建議用值傳遞「一個函式搞定」,你該怎麼寫?為什麼?
寫單一 rvalue reference overload:void setPtrstring>&& ptr) { p = std::move(ptr; },總成本 1 move。
std::unique_ptr 是 move-only:copy ctor 已 delete,本來就不需要 lvalue overload,「只寫一個函式」的優勢不存在。
值傳遞版本會 move construct 參數再 move assign 給成員 = 2 moves,成本加倍。
Question 6 - Assignment 複製的成本分析 [analysis]
Password::changeTostring newPwd) { text = std::move(newPwd; }以 lvalue 呼叫時,為何成本可能比const std::string&overload 高出數個數量級?此成本又取決於哪些因素?
值傳遞版:newPwd copy construct 需配置新記憶體,move assign 給 text 又釋放 text 舊記憶體 → 一次配置+一次釋放。
Reference overload 版:text = newPwd 在 text.capacity() >= newPwd.size() 時可重用既有記憶體,零配置零釋放——記憶體管理成本可比一次 move 高數個數量級。
成本取決於:型別是否用動態記憶體、lvalue/rvalue 比例、目標容量是否 ≥ 來源、SSO 是否容納該值——取決於「值」而非只有「型別」(rvalue 幾乎都靠 move,問題主要在 lvalue)。
實務採「有罪推定」:先用 overloading / universal reference,證明值傳遞夠快才採用。
Question 7 - push_back 字面值的隱藏成本 [recall]
std::vector<std::string> vs; vs.push_back("xyzzy");在執行期實際發生幾次std::string建構與解構?emplace_back又如何?
push_back:2 次建構 + 1 次解構——(1) 由字面值建立暫時物件 temp;(2) temp(rvalue)綁定 push_back(T&&) 後 move 建構進 vector;(3) temp 解構。
emplace_back("xyzzy"):字面值被 perfect forwarding 直達容器內部,就地建構僅 1 次,無暫時物件。
Question 8 - 介面本質差異 [recall]
insertion 函式與 emplacement 函式的參數在「意義」上有何根本不同?這解釋了什麼效率差異?
Insertion 函式接收「要插入的物件」;emplacement 函式接收「建構物件用的建構子引數」,以 perfect forwarding 直達容器內部建構點。
因此當引數型別與容器元素型別不符時,emplacement 可避免暫時物件的建構與解構——這是它效率優勢的來源。
Question 9 - 幾乎必勝三條件 [recall]
依 Item 42 的 heuristic,哪三個條件同時成立時,emplacement 幾乎必定勝過 insertion?
(1) 新值以「建構」進入容器而非「指派」(如加到尾端的 emplace_back);
(2) 傳入的引數型別異於容器持有型別(才有暫時物件可省);
(3) 容器不太可能因重複值而拒絕新值(set/map 常先建 node 再比對,重複則白付建構/解構)。
Question 10 - Copy init vs direct init [recall]
std::regex r1 = nullptr;不能編譯,std::regex r2(nullptr);卻可以——兩種語法的官方術語是什麼?與explicit建構子的關係為何?
= 語法是 copy initialization,不可使用 explicit 建構子 → 編譯錯誤;()(或 {})是 direct initialization,可以使用 explicit 建構子 → 編譯通過(但 null 不是合法 regex 字串,執行期 UB)。
對應到容器:insertion 用 copy init(擋下 explicit 轉換),emplacement 用 direct init(放行)。
Question 11 - emplace_back(nullptr) 除錯 [application]
同事寫了
std::vector<std::regex> regexes; regexes.emplace_back(nullptr);,編譯通過但程式執行期崩潰。請解釋原因,以及為何push_back(nullptr)反而編不過。
emplace_back 把 nullptr 當作 std::regex 的建構子引數,direct initialization 允許呼叫 explicit 的 regex(const char*) 建構子 → 編譯通過,但 null 指標不是合法 regex 字串 → UB(幸運的話當場崩潰)。
push_back(nullptr) 要求 nullptr → std::regex 的隱式轉換(copy init),被 explicit 擋下 → 編譯期即報錯。
教訓:用 emplacement 時要特別確認引數正確——insertion 幫你擋的型別錯誤,emplacement 會滑落到執行期。
Question 12 - 自訂 deleter 的容器插入 [application]
要把帶自訂 deleter 的
std::shared_ptr<Widget>加入std::list<std::shared_ptr<Widget>>(不能用std::make_shared),ptrs.emplace_back(new Widget, killWidget);有什麼問題?正確寫法為何?
問題:raw pointer 被 perfect forwarding 延後交給管理物件,若配置 list node 時拋出例外,尚無任何物件管理該指標 → Widget 洩漏。
正確寫法:先在獨立敘述建立管理物件,再以 rvalue 傳入——
std::shared_ptr<Widget> spw(new Widget, killWidget); ptrs.push_backmove(spw);
此時 node 配置失敗,spw 解構會呼叫 killWidget 釋放資源;且既然都付出 spw 的建構/解構成本,emplacement 已無優勢。
Question 13 - Emplacement 不必勝的情境 [analysis]
「emplacement 理論上永不比 insertion 慢」——請以
vs.emplace(vs.begin(), "xyzzy")與「向std::set加入重複值」兩個情境,分析實務上這句話為何不成立。
vs.emplace(vs.begin(), ...):目標位置已有物件,多數實作不會就地建構,而是 move assignment 把值移入——但 move assignment 需要來源物件,仍得建立暫時物件當 move 來源 → emplacement「免暫時物件」的核心優勢消失(違反三條件之 1)。
std::set 加入重複值:emplacement 實作常先建構一個 node 才能與既有節點比對,若值已存在則丟棄該 node → 建構+解構成本白付,且這種 node 在 emplacement 中比 insertion 更常建立(違反三條件之 3)。
實務效率還受引數型別、容器種類、插入位置、元素建構子的例外安全性等影響,難以簡單歸類 → 通則是兩者都 benchmark。
補充:node-based 容器幾乎一律用建構加值;非 node-based 只有 vector、deque、string,其中可放心依賴「用建構」的是 emplace_back 與 deque 的 emplace_front。
| 模式 | 觸發條件 | 答案核心 |
|---|---|---|
| 值傳遞取捨 | copyable + cheap to move + always copied + 用 construction 複製 | 一個函式解決 lvalue copy / rvalue move,代價恆多一次 move |
| 值傳遞禁區 | move-only、昂貴 move、條件式複製、assignment 複製、base class 參數、呼叫鏈 | 改用 overloading / universal reference / rvalue reference overload |
| Emplacement 優勢 | 建構進容器+引數型別異於元素型別+不查重複 | 免暫時物件:push_back("lit") 的 2 建構 1 解構 → 1 建構 |
| Emplacement 風險 | raw pointer + 自訂 deleter;explicit 建構子 |
例外安全洩漏(先建管理物件);direct init 放行錯誤引數(滑落至執行期 UB) |
| 共同精神 | 兩個 Item 都是「consider」 | 影響因素太多,成本個案評估、必要時 benchmark |