摘要訊息 : 容器的 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(...)
的形式進行建構. 一旦其被解參考, 就會發生未定行為. 然而到目前為止, 這個問題還沒有很好的解決方案...
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :