英文:
std::map try emplace vs emplace strange behaviour
问题
通常的建议是在几乎所有情况下,优先使用 std::map::try_emplace
而不是 std::map::emplace
。
我编写了一个简单的测试来追踪在调用这些函数时对象的创建、复制、移动和销毁,有冲突和没有冲突的情况下,结果显示当键不在映射中时,try_emplace
会导致键多一次移动和销毁。
为什么会有这种行为差异?
我知道移动和销毁已移动的对象通常是廉价的,尤其对于简单的键来说更是如此,但是我仍然对结果感到惊讶,因为它们似乎暗示在某些情况下,emplace
可能更有效率。
Compiler explorer 链接 (Clang 14, libc++, -O3)
源代码:
#include <map>
#include <iostream>
struct F {
F(int i): i(i) { std::cout << "- ctor (" << i << ")\n"; }
~F() { std::cout << "- dtor (" << i << ")\n"; }
F(const F& f): i(f.i) { std::cout << "- copy ctor (" << i << ")\n"; }
F(F&& f): i(f.i) { std::cout << "- move ctor (" << i << ")\n"; }
F& operator=(const F& f) { i = f.i; std::cout << "- copy (" << i << ")\n"; return *this; }
F& operator=(F&& f) { i = f.i; std::cout << "- move (" << i << ")\n"; return *this; }
bool operator <(const F& f) const { return i < f.i; }
int i{};
};
int main() {
std::map<F, F> m;
std::cout << "emplace 1:\n";
m.emplace(1, 2);
std::cout << "emplace 2:\n";
m.emplace(1, 3);
std::cout << "clear:\n";
m.clear();
std::cout << "try_emplace 1:\n";
m.try_emplace(1, 2);
std::cout << "try_emplace 2:\n";
m.try_emplace(1, 3);
std::cout << "done:\n";
}
结果:
emplace 1:
- ctor (1)
- ctor (2)
emplace 2:
- ctor (1)
- ctor (3)
- dtor (3)
- dtor (1)
clear:
- dtor (2)
- dtor (1)
try_emplace 1:
- ctor (1)
- move ctor (1)
- ctor (2)
- dtor (1)
try_emplace 2:
- ctor (1)
- dtor (1)
done:
- dtor (2)
- dtor (1)
英文:
The common advice is to prefer using std::map::try_emplace
in preference to std::map::emplace
in almost every instance.
I wrote a simple test to trace object creation/copying/moving/destruction when calling those functions, with and without clashes, and the results show that try_emplace
incurs an extra move and destruction of the key when it is not already in the map.
Why the difference in behaviour?
I do know that moves and destructions of moved-from objects are usually cheap, especially so for trivial keys, but I was still surprised by the results as they seem to imply that for some cases emplace
might be more efficient.
Compiler explorer link (Clang 14, libc++, -O3)
Source:
#include <map>
#include <iostream>
struct F {
F(int i): i(i) { std::cout << "- ctor (" << i << ")\n"; }
~F() { std::cout << "- dtor (" << i << ")\n"; }
F(const F& f): i(f.i) { std::cout << "- copy ctor (" << i << ")\n"; }
F(F&& f): i(f.i) { std::cout << "- move ctor (" << i << ")\n"; }
F& operator=(const F& f) { i = f.i; std::cout << "- copy (" << i << ")\n"; return *this; }
F& operator=(F&& f) { i = f.i; std::cout << "- move (" << i << ")\n"; return *this; }
bool operator <(const F& f) const { return i < f.i; }
int i{};
};
int main() {
std::map<F, F> m;
std::cout << "emplace 1:\n";
m.emplace(1, 2);
std::cout << "emplace 2:\n";
m.emplace(1, 3);
std::cout << "clear:\n";
m.clear();
std::cout << "try_emplace 1:\n";
m.try_emplace(1, 2);
std::cout << "try_emplace 2:\n";
m.try_emplace(1, 3);
std::cout << "done:\n";
}
Results:
emplace 1:
- ctor (1)
- ctor (2)
emplace 2:
- ctor (1)
- ctor (3)
- dtor (3)
- dtor (1)
clear:
- dtor (2)
- dtor (1)
try_emplace 1:
- ctor (1)
- move ctor (1)
- ctor (2)
- dtor (1)
try_emplace 2:
- ctor (1)
- dtor (1)
done:
- dtor (2)
- dtor (1)
答案1
得分: 5
以下是翻译好的部分:
两个函数之间的区别是:
std::map::emplace
在原地构造一个value_type
,即一个std::pair
,然后尝试插入这个对。std::map::try_emplace
首先尝试找到插入位置,如果找到了,它将在原地构造value_type
,即std::pair
。
try_emplace
看起来可以为我们节省一些工作,但请记住,即使我们使用 try_emplace
不向地图插入任何新内容,我们仍然需要检查是否可以插入以及在哪里插入。这默认使用 std::less
来完成,最终调用以下函数:
bool operator<(const F& f) const { return i < f.i; }
为了执行此比较,必须存在一个 F
对象,并且在 try_emplace
中会构造一次。
让我们使用正在发生的情况来注释您的示例:
emplace 1:
- 构造函数 (1) // 构造 node{1, 2}
- 构造函数 (2)
emplace 2:
- 构造函数 (1) // 构造 node{1, 3}
- 构造函数 (3)
- 析构函数 (3) // 无法插入,销毁 node{1, 3}
- 析构函数 (1)
clear:
- 析构函数 (2) // 销毁 node{1, 2}
- 析构函数 (1)
try_emplace 1:
- 构造函数 (1) // 构造临时键
- 移动构造函数 (1) // 找到位置,将键移动到 node
- 构造函数 (2) // 构造值
- 析构函数 (1) // 销毁临时键
try_emplace 2:
- 构造函数 (1) // 构造临时键
- 析构函数 (1) // 销毁临时键
done:
- 析构函数 (2) // 销毁 node{1, 2}
- 析构函数 (1)
结论
我们不能说 emplace
比 try_emplace
通常更好或更差,而是存在权衡:
无预先存在的键 | 插入成功 | 插入失败 |
---|---|---|
emplace |
零额外开销<sup>1)</sup> | 浪费键和值的初始化 |
try_emplace |
浪费键的移动 | 浪费键的初始化 |
当我们有一个预先存在的键而不是在 try_emplace
或 emplace
中初始化键时,权衡会发生变化:
预先存在的键 | 插入成功 | 插入失败 |
---|---|---|
emplace |
浪费键的移动 | 浪费键的移动和初始化,浪费值的初始化 |
try_emplace |
浪费键的移动 | 零额外开销 |
总之,与 emplace
相比,try_emplace
最坏的情况下浪费一个移动构造函数,最好的情况下严格更好。在大多数情况下更喜欢它,但不是所有情况都是如此。
英文:
The difference between the two functions is:
std::map::emplace
constructs avalue_type
, i.e. astd::pair
in-place, and then attempts to insert this pair.std::map::try_emplace
attempts to find an insert location first, and if one is found, it will construct thevalue_type
, i.e. astd::pair
in-place.
try_emplace
appears to save us some work, but remember that even if we don't insert anything new into the map with try_emplace
, we still have to check whether we can, and where. This happens with std::less
by default, which ends up calling:
bool operator<(const F& f) const { return i < f.i; }
An F
object must exist to perform this comparison, and is being constructed once during try_emplace
.
Let's annotate your example with what is happening:
emplace 1:
- ctor (1) // construct node{1, 2}
- ctor (2)
emplace 2:
- ctor (1) // construct node{1, 3}
- ctor (3)
- dtor (3) // can't insert, destroy node{1, 3}
- dtor (1)
clear:
- dtor (2) // destroy node{1, 2}
- dtor (1)
try_emplace 1:
- ctor (1) // construct temporary key
- move_ctor (1) // location found, move key to node
- ctor (2) // construct value
- dtor (1) // destroy temporary key
try_emplace 2:
- ctor (1) // construct temporary key
- dtor (1) // destroy temporary key
done:
- dtor (2) // destroy node{1, 2}
- dtor (1)
Conclusion
We can't say that emplace
is universally better or worse than try_emplace
, rather there is a trade-off:
No Pre-Existing Key | Insertion Success | Insertion Failure |
---|---|---|
emplace |
zero overhead<sup>1)</sup> | wasted key and value initialization |
try_emplace |
wasted key move | wasted key initialization |
The trade-off changes when we have a pre-existing key instead of initializing one inside of try_emplace
or emplace
:
Pre-Existing Key | Insertion Success | Insertion Failure |
---|---|---|
emplace |
wasted key move | wasted key move & init, wasted value init |
try_emplace |
wasted key move | zero overhead |
In conclusion, try_emplace
is at worst wasting one move constructor compared to emplace
, and at best, it's strictly better. Prefer it in most, but not all cases.
<sup>1)</sup> "overhead" is relative to magically knowing the right location (or absence thereof) in the map and initializing a node in-place. Initialization of the pre-existing key is considered free.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论