为何std::unordered_map insert的&&重载会被选中?性能差异解析
首先要明确几个关键点:std::unordered_map<K, V>的value_type是std::pair<const K, V>,而insert的重载不止你提到的两个——还有一个完美转发的模板重载,这才是问题的核心:
template <class P> std::pair<iterator, bool> insert(P&& value);
C++的重载决议规则会优先选择匹配度更高的重载,而非模板函数仅在和模板函数匹配度相同时才会被优先选中。结合你的问题,我们分两种场景分析:
场景1:你的描述——p是std::pair<const string, int>(即value_type)
假设我们有:
std::unordered_map<std::string, int> map; std::pair<const std::string, int> p = {"key", 42}; // non-const左值 map.insert(p);
此时可选的重载:
insert(const value_type&):需要将非const的p转换为const value_type&(添加const限定符,属于资格转换),可行但需要一次转换。- 模板重载
insert(P&&):模板参数P会被推导为value_type&,参数折叠后为value_type&,和p的类型完全匹配,不需要任何转换,匹配度更高。
所以模板重载会被选中。但这里并不会触发移动操作——因为std::forward<P>(p)是左值引用,内部会调用value_type的拷贝构造函数,性能应该和const版本接近。你提到的“慢一倍”更可能是混淆了p的类型,也就是下面的场景。
场景2:Matt演讲中的实际场景——p是std::pair<string, int>(非value_type)
这才是演讲里的真实情况:
// Benchmark_Slow std::unordered_map<std::string, int> map; std::pair<std::string, int> p = {"key", 42}; // non-const左值 map.insert(p); // Benchmark_Fast const std::pair<std::string, int> p_const = {"key", 42}; map.insert(p_const);
为什么Benchmark_Slow更慢?
对于非const的p:
模板重载insert(P&&)会被优先选中(因为P推导为std::pair<string, int>&,参数完全匹配p,不需要转换)。内部需要将std::pair<string, int>转换为value_type(std::pair<const string, int>):- 首先构造一个临时的
value_type对象:由于p是左值,会调用pair的模板转换构造函数,拷贝p的string成员(因为是左值,无法移动)。 - 这个临时
value_type是右值,会被传递给哈希表的节点构造——但value_type的first是const string,无法移动(移动会修改原对象,而const string不允许修改),所以又会拷贝一次string。
最终是两次string拷贝,性能自然慢一倍。
- 首先构造一个临时的
对于const的p_const:
此时有两个可行重载:- 非模板重载
insert(const value_type&):需要将const pair<string, int>转换为value_type(临时对象),可行。 - 模板重载
insert(P&&):参数完全匹配const pair<string, int>&,可行。
根据C++重载规则,非模板函数优先级高于模板函数,所以会选中insert(const value_type&)。此时仅需要一次string拷贝(构造临时value_type时),哈希表节点直接从这个临时对象拷贝构造,总拷贝次数是一次,所以更快。
- 非模板重载
总结
你描述的p是value_type的场景下,性能差异不会那么明显;而Matt演讲中的真实场景是p为非value_type的pair<string, int>,此时非const版本会触发两次string拷贝,const版本仅一次,这就是“慢一倍”的原因。核心在于模板重载的匹配优先级,以及const string无法移动导致的额外拷贝。
内容的提问来源于stack exchange,提问作者user823255

