回顧 emplace
大家的好朋友 Standard Template Library (STL) 容器提供如 push_back
, insert
等介面,讓我們塞東西進去; C++11 之後,新增了 emplace
系列的介面,如 std::vector::emplace_back
, std::map::emplace
等,差異在於 emplace
是在容器內 in-place 直接建構新元素,而不像 push_back
在傳遞參數前建構,下面用實例來說明:
struct Value {
// ctor1
Value(int size) : array(new char[size]), size(size) {
printf("ctor1: %d\n", size);
}
// ctor2
Value(const Value& v) : array(new char[v.size]), size(v.size) {
printf("ctor2: %d\n", size);
memcpy(array.get(), v.array.get(), size);
}
private:
std::unique_ptr<char[]> array;
int size = 0;
};
struct Value
定義了自訂建構子 (ctor1),以指定大小 size
配置陣列,複製建構子 (ctor2) 則會配置與來源相同大小及內容的陣列,為了方便觀察加了一些 printf
。當我們如下使用 std::vector::push_back
時
std::vector<Value> v;
v.push_back(Value(2048));
首先 Value 會先呼叫 ctor1,傳給 push_back
之後再用來初始化 (呼叫 ctor2) 容器內的新元素,所以印出來的資訊是:
ctor1: 2048
ctor2: 2048
使用 emplace_back
的版本:
v.emplace_back(2048);
可以發現不需要加上 Value()
的建構了,emplace_back
會將參數 (2048) 當成新元素的初始化參數直接在容器內建構,這時印出來的資訊會是
ctor1: 2048
附帶一提,若讓 Value
支援移動建構式 (move constructor) 例如:
Value(Value &&v)
: array(std::move(v.array)), size(std::move(v.size)) {
printf("ctor3: no copy!\n");
}
push_back
版本仍需呼叫兩種建構式 (ctor1, ctor3),但可免除配置陣列與複製內容的運算。
為何需要 try_emplace
std::map
或是 std::unordered_map
都須保證鍵值 (key) 的唯一性,當插入新元素遇到重複鍵值時,該操作會失敗,呼叫者可透過回傳值來判斷操作成功與否,然而遇到 N42701 提出的例子時,程式碼就變得較不直覺
std::map<std::string, std::unique_ptr<Foo>> m;
m["foo"] = nullptr;
auto ptr = std::make_unique_ptr<Foo>;
auto res = m.emplace("foo", std::move(ptr));
assert(ptr); // ???
由於 emplace
會失敗,然而 C++11 中並未定義新元素的建構時機 (p746, $23.2.4, N63902),也就是看 STL 實作的方式,即使遇到 emplace
失敗,新元素可能已經被建立再銷毀了;以前面的例子來說,ptr
可能已經被移動到新元素中跟著被釋放掉了(註)。為了避免這個問題,C++11 時我們得先用 find
確認鍵值是否存在,再進行 emplace
操作;因此,C++17 納入了 try_emplace
,此介面保證在插入失敗時,不會偷取建構參數。
註:std::move(ptr)
是強制把 ptr
當成右值,如果沒有被用來建構新元素,並不會真的被移動。
留言
張貼留言