关于std::set/std::unordered_set::emplace()的构造优化与实现疑问
有一个有趣的性能现象:当传入的T类型对象已存在于容器中时,朴素实现的std::unordered_set<T>::emplace()比insert()要慢。原因在于insert()可以直接检测到元素已存在并立即退出,而朴素的emplace()会先分配节点、构造元素,再检查唯一性,若发现元素已存在则销毁元素并释放节点。
当emplace()接收的参数数量≠1时,这种额外开销无法避免——毕竟不调用构造函数就无法解析参数。但传入1个参数的情况,理论上存在优化空间。这一现象同样适用于std::set<T>::emplace()。
基于此提出两个核心问题:
1. C++标准是否允许这种优化?
即当emplace()仅传入1个参数,且remove_cvref_t<argument-type>与元素类型匹配时,emplace()能否直接对参数应用==/</哈希操作,而无需构造新对象?
延伸:透明比较器场景
如果容器使用了透明比较器,且emplace()的参数可直接与元素类型比较,是否允许跳过完整元素的构造,直接进行比较?
2. 当前主流实现是否实际采用了这种优化?
问题解答
关于标准允许性
C++标准允许这种优化。标准对emplace()的要求是:仅当元素确实需要被插入时,才构造该元素;若元素已存在,允许实现避免不必要的构造操作。
对于单参数且类型匹配的场景,标准并未强制要求必须先构造元素再检查唯一性。只要实现能正确完成“检查存在性→仅在不存在时构造插入”的逻辑,就符合标准要求。
对于透明比较器的情况,标准同样允许优化:当参数类型可被比较器直接用于与容器元素比较时,实现可以跳过元素构造,直接用参数执行存在性检查,仅在需要插入时才构造元素。
关于主流实现的优化情况
目前主流的C标准库实现(如GCC的libstdc、Clang的libc++、MSVC的STL)大多已实现了这种优化:
- 对于单参数且类型匹配的
emplace()调用,会先通过参数执行存在性检查(哈希+相等性比较,或有序比较),仅当元素不存在时才分配节点并构造元素。 - 在透明比较器的场景下,这类优化会进一步扩展:只要参数能被比较器处理,即使参数类型与元素类型不完全匹配,也会先尝试用参数检查存在性,避免不必要的构造。
内容的提问来源于stack exchange,提问作者HolyBlackCat

