Item 42:考慮以 emplacement 取代 insertion (Consider Emplacement Instead of Insertion)
Overview Table
| 面向 | Insertion(push_back/insert…) |
Emplacement(emplace_back/emplace…) |
|---|---|---|
| 參數意義 | 接收「要插入的物件」 | 接收「建構物件用的建構子引數」 |
| 傳遞機制 | 型別不符時先建立暫時物件 | perfect forwarding 直達容器內部建構點 |
| 暫時物件成本 | 可能多 1 次建構 + 1 次解構 | 通常可避免暫時物件 |
| 初始化語意 | copy initialization(不可用 explicit 建構子) |
direct initialization(可用 explicit 建構子) |
| 理論效率 | 基準 | 有時更快,理論上永不更慢 |
| 實務效率 | 某些情境反而較快 | 視情境而定 → benchmark |
| 型別安全 | 較嚴格(拒絕隱式轉換被 explicit 擋下的引數) |
較寬鬆 → 可能悄悄接受錯誤引數(如 nullptr) |
- 原則上 emplacement 函式有時比對應的 insertion 函式更有效率,且不應更沒效率。
- 實務上最可能更快的三條件:(1) 新值是建構進容器而非指派;(2) 引數型別異於容器持有型別;(3) 容器不會因重複值而拒絕新值。
- Emplacement 函式可能執行 insertion 函式會拒絕的型別轉換(
explicit建構子)。
機制差異:暫時物件 vs 就地建構
容器持有 std::string,但手上是 string literal(const char[6]),型別不符:
std::vector<std::string> vs; // 持有 std::string 的容器
vs.push_back("xyzzy"); // 等同 vs.push_backstring("xyzzy")
// → 產生暫時 std::string,2 次建構 + 1 次解構
vs.emplace_back("xyzzy"); // 引數完美轉發進容器,就地建構
// → 只有 1 次建構,無暫時物件
vs.emplace_back(50, 'x'); // 可轉發任意引數組合:
// 呼叫 string(50, 'x') 建構 50 個 'x'
vs.push_back("xyzzy") vs.emplace_back("xyzzy")
│ │
▼ ▼
[1] 建立暫時物件 temp("xyzzy") "xyzzy" 以 perfect forwarding
← 第 1 次 string 建構 直達容器內部建構點
│ (temp 是 rvalue) │
▼ ▼
[2] temp 綁定 push_back(T&&) 在 vector 記憶體中直接
→ move 建構進 vector 建構 std::string
← 第 2 次 string 建構 ← 僅 1 次建構
│
▼
[3] temp 解構 ← 1 次 string 解構
───────────────────────── ─────────────────────────
成本:2 建構 + 1 解構 成本:1 建構(無暫時物件)
每個支援 insertion 的標準容器都有對應的 emplacement 版本:
| Insertion 函式 | Emplacement 對應 | 適用容器 |
|---|---|---|
push_back |
emplace_back |
vector、deque、list、string |
push_front |
emplace_front |
deque、list、forward_list |
insert |
emplace |
除 forward_list、array 外皆支援 |
insert(帶 hint 迭代器) |
emplace_hint |
關聯式容器 |
insert_after |
emplace_after |
forward_list |
現行標準函式庫實作中,確實存在 insertion 較快的情境,且難以簡單歸類(取決於引數型別、容器種類、插入位置、元素建構子的例外安全性、是否禁止重複值等)。效能調校的通則依然適用:兩者都 benchmark。
Emplacement 幾乎必勝的三條件(heuristic)
| 條件 | 說明 | 反例 |
|---|---|---|
| 1. 值以「建構」進入容器,而非「指派」 | 加到尚無物件的位置(如尾端)→ 就地建構 | vs.emplace(vs.begin(), "xyzzy"):位置已有物件,多數實作改用 move assignment,仍需暫時物件當 move 來源 → 優勢消失 |
| 2. 引數型別異於容器持有型別 | 避免為滿足 insertion 介面而生的暫時物件 | 傳入的本來就是 T(如 vs.emplace_back(existingStr))→ 無暫時物件可省,與 insertion 等效 |
| 3. 容器不太可能因重複值拒絕新值 | 容器允許重複,或多數新值唯一 | set/map 等禁止重複的容器:emplacement 實作常先建一個 node 才能比對,若值已存在則丟棄 node → 白付建構/解構成本 |
std::string queenOfDisco("Donna Summer");
vs.push_back(queenOfDisco); // copy 建構到 vs 尾端
vs.emplace_back(queenOfDisco); // 效果完全相同(違反條件 2,無效率優勢)
node-based 容器(list、set、map 及 unordered 系列等多數標準容器)幾乎一律用建構加入新值;非 node-based 的只有 std::vector、std::deque、std::string。其中可以放心依賴「用建構」的是 emplace_back(vector/deque/string),以及 std::deque 的 emplace_front。
陷阱一:資源管理物件與例外安全
對持有 std::shared_ptr(自訂 deleter,故不能用 std::make_shared)的容器,emplacement 的「省暫時物件」反而打開資源洩漏的窗口:
std::list<std::shared_ptr<Widget>> ptrs;
void killWidget(Widget* pWidget); // 自訂 deleter
ptrs.push_back({ new Widget, killWidget }); // 先建暫時 shared_ptr temp;
// 若配置 list node 時拋出例外,
// temp 解構 → killWidget 釋放 Widget
// → 不洩漏
ptrs.emplace_back(new Widget, killWidget); // raw pointer 被完美轉發;
// 若配置 node 時拋出例外,
// 尚無任何物件管理該指標
// → Widget 洩漏!
正確做法(Item 21 的原則):先在獨立敘述中把資源交給資源管理物件,再以 rvalue 傳入——此時 emplacement 相對 insertion 已無優勢:
std::shared_ptr<Widget> spw(new Widget, killWidget); // 先讓 spw 接管資源
ptrs.push_backmove(spw); // 以 rvalue 加入
// emplace_backmove(spw) 亦可,但成本相同:都付出建構/解構 spw 的代價
shared_ptr/unique_ptr 的有效性建立在「資源立即交給資源管理物件」上。emplacement 的 perfect forwarding 會延後管理物件的建構,例外一旦介入即洩漏。所有標準容器皆有此問題——別用程式效率換取例外安全性的下降。
陷阱二:explicit 建構子與 direct initialization
Emplacement 使用 direct initialization,因此可以呼叫 explicit 建構子;insertion 使用 copy initialization,不可以:
std::vector<std::regex> regexes;
regexes.push_back(nullptr); // 錯誤!不能編譯:copy init
// 禁用 explicit 的 regex(const char*) 建構子
regexes.emplace_back(nullptr); // 可編譯!direct init 允許 explicit 建構子
// → 等同 std::regex r(nullptr)
// → 未定義行為(null 不是合法 regex 字串)
| 語法 | 官方術語 | 可用 explicit 建構子? |
|---|---|---|
std::regex r1 = nullptr; |
copy initialization | 否(不能編譯) |
std::regex r2(nullptr); |
direct initialization | 是(編譯過但 UB) |
push_back(nullptr) |
copy initialization | 否(編譯期擋下) |
emplace_back(nullptr) |
direct initialization | 是(悄悄放進錯誤值) |
編譯器會為了讓程式碼合法而把 explicit 建構子也納入考慮——insertion 幫你擋下的型別錯誤,emplacement 不會。傳錯引數時,錯誤從編譯期滑落到執行期(甚至 UB)。
emplace_back 以 perfect forwarding 實作,故 braced initializer、0/NULL 作為 null 指標、bitfield 等 perfect forwarding 失敗案例(Item 30)也會讓 emplacement 失效或行為意外。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
push_back("literal") 的實際成本 |
建立暫時 std::string:2 次建構 + 1 次解構;emplace_back 僅 1 次建構 |
| insertion vs emplacement 參數的本質差異 | insertion 收「物件」,emplacement 收「建構子引數」(perfect forwarding) |
| emplacement 何時幾乎必快(三條件) | 建構非指派進容器、引數型別異於元素型別、不會被當重複值拒絕 |
emplace(vs.begin(), ...) 為何不一定快 |
目標位置已有物件 → 實作多用 move assignment,仍需暫時物件 |
傳入型別已是 T 時選哪個 |
兩者等效,無效率差異;不確定就 benchmark |
set/map 用 emplace 加入重複值 |
常先建 node 再比對,重複則丟棄 → 建構/解構成本白付 |
emplace_back(new Widget, deleter) |
例外安全漏洞:node 配置失敗時 raw pointer 洩漏;應先建 shared_ptr 再 push_backmove(spw) |
regexes.emplace_back(nullptr) 可編譯? |
可(direct init 允許 explicit 建構子)但為 UB;push_back(nullptr) 則編譯錯誤 |
| copy init vs direct init | = 是 copy init(禁 explicit);()/{} 是 direct init(允許 explicit) |
| 「emplacement 永不更慢」對嗎? | 只在理論上;實務有 insertion 較快的情境,需實測 |
Related Notes
- 09-Tweaks/01-Pass-By-Value — 同章 Item 41:另一個「consider」級技巧;同樣以 copy/move 成本計算決定取捨
- 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — Item 21:make 函式與「資源立即交給管理物件」原則,本 Item 例外安全陷阱的根源
- 04-Moving-To-Modern-Cpp/01-Braced-Initialization — Item 7:copy init vs direct init 的語法背景,以及
()與{}的差異 - 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases — Item 30:emplacement 依賴 perfect forwarding,其失敗案例直接限制 emplacement
- 05-Smart-Pointers/02-Shared-Ptr — Item 19:自訂 deleter 的
std::shared_ptr,本 Item 洩漏情境的主角