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 initializationexplicit 建構子)
理論效率 基準 有時更快,理論上永不更慢
實務效率 某些情境反而較快 視情境而定 → benchmark
型別安全 較嚴格(拒絕隱式轉換被 explicit 擋下的引數) 較寬鬆 → 可能悄悄接受錯誤引數(如 nullptr
Things to Remember(Item 42)

  1. 原則上 emplacement 函式有時比對應的 insertion 函式更有效率,且不應更沒效率。
  2. 實務上最可能更快的三條件:(1) 新值是建構進容器而非指派;(2) 引數型別異於容器持有型別;(3) 容器不會因重複值而拒絕新值。
  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 vectordequeliststring
push_front emplace_front dequelistforward_list
insert emplace forward_listarray 外皆支援
insert(帶 hint 迭代器) emplace_hint 關聯式容器
insert_after emplace_after forward_list
「emplacement 永不更慢」只在理論上成立

現行標準函式庫實作中,確實存在 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,無效率優勢)
建構 vs 指派由實作決定,但有規律可循

node-based 容器listsetmap 及 unordered 系列等多數標準容器)幾乎一律用建構加入新值;非 node-based 的只有 std::vectorstd::dequestd::string。其中可以放心依賴「用建構」的是 emplace_backvector/deque/string),以及 std::dequeemplace_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 (悄悄放進錯誤值)
使用 emplacement 時要特別確認引數正確

編譯器會為了讓程式碼合法而把 explicit 建構子也納入考慮——insertion 幫你擋下的型別錯誤,emplacement 不會。傳錯引數時,錯誤從編譯期滑落到執行期(甚至 UB)。

Perfect forwarding 的限制同樣適用

emplace_back 以 perfect forwarding 實作,故 braced initializer、0/NULL 作為 null 指標、bitfield 等 perfect forwarding 失敗案例(Item 30)也會讓 emplacement 失效或行為意外。

Exam/Test Patterns

情境關鍵字 答案
push_back("literal") 的實際成本 建立暫時 std::string2 次建構 + 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_ptrpush_backmove(spw)
regexes.emplace_back(nullptr) 可編譯? 可(direct init 允許 explicit 建構子)但為 UBpush_back(nullptr) 則編譯錯誤
copy init vs direct init = 是 copy init(禁 explicit);()/{} 是 direct init(允許 explicit
「emplacement 永不更慢」對嗎? 只在理論上;實務有 insertion 較快的情境,需實測