C++ 11 引入的完美轉遞使得容器的放置操作成為可能. C++ 11 也為大部分容器和容器配接器添加了放置操作 :

  • 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 (C++ 17)
  • std::multiset : emplace, emplace_hint
  • std::multimap : emplace, emplace_hint
  • std::unordered_map : emplace, emplace_hint, try_emplace (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

放置帶來的方便已經不需要我多說了, 它支援直接從給定的引數向對應型別轉型, 也支援複製和移動. 對於一般的插入來說, 放置會通過原地建構的方式, 然後以移動指派或者複製指派的方式放入到對應的位置. 對於在頭部位置和尾部位置的放置, 在可能的情況下, 會直接在原地進行建構. 這種性能是 insertpush_backpush_front 等函式沒有辦法媲美的. 最多的情況下, 放置操作比普通的插入操作少了一次建構、一次移動和一次解構

這篇文章並不是要說放置操作有多麼得好, 而是要說放置操作有時候也會引起一些問題, 就和完美轉遞並不完美一樣

1. 當容器和記憶體配置相關聯的時候

考慮下面的程式碼 :

#include <forward_list>

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

你知道放置操作帶來的好處, 於是你寫下了上述程式碼. 上述程式碼一般來說沒什麼問題, 但是在記憶體較少的裝置上會存在著例外安全問題. 因為 std::forward_list 在執行插入的時候 (放置的本質也是插入), 總是需要配置一個結點然後在結點內部進行放置. 上述程式碼中的 new int 會配置一段未被存儲位址的記憶體, 一旦結點在配置的時候擲出例外情況, 那麼由 new int 配置而來的記憶體會流失. 我們曾經提過, 對於智慧指標的建構, 儘量使用 make 系列函式, 上述問題就可以被解決. 或者提前存下記憶體位址, 在捕獲到例外情況之後進行回收

上面的問題不僅僅出現在 std::forward_list 中, 同時還出現在每一個會增長的容器中

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

設有一個類別 reg, 它是用於字串匹配, 其有一個被 explicit 標識, 接受引數型別為 const char * 的建構子 :

class reg {
private:
    const char *str;
public:
    explicit reg(const char *);
    //...
};

我們知道, 在字串匹配的過程中需要解參考, 因此 str 不可以是 nullptr, 否則會造成錯誤. 由於禁止了 const char *reg 的隱含型別轉化, 所以以下程式碼無法通過編碼 :

#include <vector>

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

這是因為 std::vectorpush_back 成員函式僅僅接受型別為 const reg & 或者 reg && 的引數, 而 const char * 並不存在到 reg 的隱含型別轉化, 所以上述程式碼會出現編碼錯誤. 然而, 放置操作會吃掉這個編碼錯誤, 從而使得程式隱含地存在問題 :

#include <vector>

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

這是因為放置操作會在原地進行建構. 我寫一個簡化的 std::vector :

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

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

一旦你不注意寫出了類似的程式碼, 你只能期望那些類別作者會幫你擦屁股...