unordered_map::emplace调用时,key采用val.str原始值还是移动后的值?
你的emplace调用会不会用被移动后的str当键?
这个问题问到点子上了——这其实是C++里函数参数求值顺序和类成员初始化顺序的经典坑,不同标准版本下的表现还不一样,咱们一步步说清楚:
先给结论:C17前有风险,C17后也不建议赌运气
你的担忧完全合理:如果编译器先处理std::move(val)再处理val.str,那val.str已经被移动走(变成空或者未指定状态),这时候键就错了。而这个顺序在C17之前是编译器说了算的,属于未指定行为;C17虽然优化了部分求值顺序,但并没有强制要求emplace的参数必须从左到右求值,所以依然存在隐患。
为什么会有这个问题?
咱们拆解成两个关键点:
- 函数参数的求值顺序:在C++17之前,函数的多个参数求值顺序是未指定的——编译器可以先算右边的
std::move(val),再算左边的val.str。一旦这么做,val已经被移动,val.str的状态就不确定了。 - std::pair的初始化顺序:虽然
std::pair的成员是先first(键)后second(值)声明的,初始化也会按这个顺序来,但这是进入pair构造函数之后的事。如果参数求值时先移动了val,那构造键的时候用的已经是被移动后的str了。
怎么写才安全?
不管用哪个C++版本,最稳妥的做法有两种:
方案一:提前把键存下来
先复制出val.str的值,再移动val,彻底切断两者的依赖:
void foo(Foo&& val) { std::unordered_map<std::string, Foo> m; std::string key = val.str; // 先把键拿出来,避免后续移动影响 m.emplace(std::move(key), std::move(val)); // 这里key可以移动,不影响 }
方案二:用piecewise_construct明确初始化顺序
std::piecewise_construct是专门用来解决这种需要分别初始化pair两个成员的场景,它会先构造键,再构造值,完全避开参数求值顺序的问题:
#include <tuple> // 需要包含tuple头文件 void foo(Foo&& val) { std::unordered_map<std::string, Foo> m; m.emplace( std::piecewise_construct, std::forward_as_tuple(val.str), // 专门给键的初始化参数 std::forward_as_tuple(std::move(val)) // 专门给值的初始化参数 ); }
这种方式下,键肯定是用val.str的原始值构造的,之后才会移动val来初始化值,彻底没风险。
总结
- 你的原始代码在C17之前可能出现键错误的情况,C17后也不建议依赖编译器的求值顺序;
- 提前保存键或者用
piecewise_construct是最安全的写法,能在所有标准版本下保证行为正确。
内容的提问来源于stack exchange,提问作者j6t
相关产品推荐
相关产品推荐

