摘要訊息 : 容器的 emplace 系列也沒有想像的那麼完美.

0. 前言

C++ 11 引入了完美轉遞, 完美轉遞為容器直接建構元素提供了便利, 避免了複製和移動. 對於標準樣板程式庫中的容器, C++ 11 也為除了 std::array 之外的容器和容器配接器添加了放置操作 :

  • std::vector : emplace, emplace_back;
  • std::list : emplace, emplace_front, emplace_back;
  • std::deque : emplace, emplace_front, emplace_back;
  • std::forward_list : emplace_after, emplace_front;
  • std::set : emplace, emplace_hint;
  • std::map : emplace, emplace_hint, try_emplace (since C++ 17);
  • std::multiset : emplace, emplace_hint;
  • std::multimap : emplace, emplace_hint;
  • std::unordered_map : emplace, emplace_hint, try_emplace (since C++ 17);
  • std::unordered_set : emplace, emplace_hint;
  • std::unordered_multimap : emplace, emplace_hint;
  • std::unordered_multiset : emplace, emplace_hint;
  • std::stack : emplace;
  • std::queue : emplace;
  • std::priority_queue : emplace.

放置操作不但支援直接從給定的引數向對應型別建構, 也支援複製和移動. 對於一般的插入來說, 其實也可以委託給放置操作. 最多的情況下, 放置操作比普通的插入操作少了一次引數的建構, 一次複製或者移動和一次引數的解構. 但是放置操作有時候也會引起一些問題, 就和完美轉遞並那麼不完美 (《【C++】移動, 通用參考和完美轉遞》) 一樣.

更新紀錄 :

  • 2022 年 5 月 29 日進行第一次更新和修正.

1. 當容器元素和記憶體配置相關聯

#include <memory>
#include <forward_list>

int main(int argc, char *argv[]) {
    std::forward_list<std::shared_ptr<int>> l;
    l.emplace_front(new int);
}

Code 1 看似沒什麼太大問題, 但是在記憶體較少的裝置上會存在著例外安全問題. 因為 std::forward_list 在執行插入的時候 (放置的本質也是插入), 總是需要配置一個新的結點, 然後在這個新結點內部直接建構元素. 上述程式碼中的 new int 會配置一段未被存儲位址的記憶體, 一旦結點在配置的時候擲出例外情況, 那麼由 new int 配置而來的記憶體會流失. 根據《【C++】智慧指標》, 我們應該改成使用 std::shared_ptr 的建構子或者 std::make_shared 進行建構. 另外, 還可以提前宣告變數存下這個記憶體位址, 在捕獲到例外情況之後進行回收.

這樣的問題不僅僅在 std::forward_list 中存在, 也在除了 std::array 之外的各個容器中存在. 只要容器會增長, 就會存在容器內部的記憶體配置.

2. 當放置操作和指標配合

設有一個類別 reg, 它是用於字串匹配, 其有一個被 explicit 標識, 接受引數型別為 const char * 的建構子. 我們知道, 在字串匹配的過程中需要解參考, 因此 reg 內部儲存的字面值字串不可以是 nullptr, 否則會造成錯誤. 由於使用 explicit 禁止了從 const char *reg 的隱含型別轉化, 所以以下程式碼無法通過編碼 :

#include <vector>

class reg {
private:
    const char *str;
public:
    explicit reg(const char *);
    //...
};
int main(int argc, char *argv[]) {
    std::vector<reg> regs;
    regs.push_back(nullptr);        // Error
}

然而, 如果我們改成 regs.empalce_back(nullptr); 就可以編碼通過了. 這是因為放置操作會在原地進行建構. 參考這個簡化的 vector :

#include <utility>

class reg_vector {
private:
    reg *first;
    reg *cursor;
    reg *last;
public:
    void push_back(const reg &value) {
        new (this->cursor) T(value);        // copy construction
        ++this->cursor;
    }
    void push_back(reg &&value) {
        new (this->cursor) T(std::move(value));     // move construction
        ++this->cursor;
    }
    template <typename ...Args>
    void emplace_back(Args &&...args) {
        new (this->cursor) T(std::forward<Args>(args)...);
        ++this->cursor;
    }
    // ...
};

我們看到, 成員函式 push_back 是直接接受一個建構好且型別為 reg 的引數, 但是 emplace_back 接受任何型別的引數. 雖然 const char * 並不能直接通過隱含型別轉化轉型到 reg 型別, 但是可以通過 reg(...) 的形式進行建構. 一旦其被解參考, 就會發生未定行為. 然而到目前為止, 這個問題還沒有很好的解決方案...