移动语义与大括号初始化下构造函数参数的求值顺序问题
构造函数按值传参时的求值顺序陷阱
我们都知道std::move本身不会执行移动操作,它只是把对象转换为右值引用。所以正常情况下,调用类似void f(std::vector<int>, std::size_t)的函数时,哪怕std::move(v)比v.size()先求值,原v也不会被移动,size()会返回原值。
但有一种特殊场景会打破这个逻辑:当构造函数按值接收vector,且使用大括号初始化时,行为会出现异常。以下是测试代码:
#include <vector> #include <fmt/core.h> struct ByValue { std::vector<int> v; std::size_t x; ByValue(std::vector<int> v, std::size_t x) : v(std::move(v)), x(x) {} }; struct ByRValueRef { std::vector<int> v; std::size_t x; ByRValueRef(std::vector<int>&& v, std::size_t x) : v(std::move(v)), x(x) {} }; template <typename T> void test(std::string_view name) { { auto v = std::vector<int>(42); // 大括号初始化 auto s = T{std::move(v), v.size()}; fmt::print("{}{{std::move(v), v.size()}} => s.x == {}\n", name, s.x); } { auto v = std::vector<int>(42); // 小括号构造 auto s = T(std::move(v), v.size()); fmt::print("{}(std::move(v), v.size()) => s.x == {}\n", name, s.x); } } std::size_t getXValue(std::vector<int>, std::size_t x) { return x; } std::size_t getXRValueRef(std::vector<int>&&, std::size_t x) { return x; } int main() { test<ByValue>("ByValue"); test<ByRValueRef>("ByRValueRef"); { auto v = std::vector<int>(42); fmt::print("getXValue -> {}\n", getXValue(std::move(v), v.size())); } { auto v = std::vector<int>(42); fmt::print("getXRValueRef -> {}\n", getXRValueRef(std::move(v), v.size())); } }
GCC 输出
ByValue{std::move(v), v.size()} => s.x == 0 ByValue(std::move(v), v.size()) => s.x == 42 ByRValueRef{std::move(v), v.size()} => s.x == 42 ByRValueRef(std::move(v), v.size()) => s.x == 42 getXValue -> 42 getXRValueRef -> 42
Clang 输出
ByValue{std::move(v), v.size()} => s.x == 0 ByValue(std::move(v), v.size()) => s.x == 0 ByRValueRef{std::move(v), v.size()} => s.x == 42 ByRValueRef(std::move(v), v.size()) => s.x == 42 getXValue -> 0 getXRValueRef -> 42
问题解析
大括号初始化的求值顺序:
C++标准明确规定,列表初始化(大括号形式)的实参求值顺序是严格从左到右。所以T{std::move(v), v.size()}会先处理std::move(v)——由于ByValue的构造函数按值接收参数,这里会触发std::vector的移动构造函数,直接掏空原v;之后再调用v.size()自然返回0,这是符合标准的行为。普通函数/构造调用的求值顺序:
对于小括号形式的构造或普通函数调用,C++标准未指定实参的求值顺序,编译器可以自由选择求值顺序:- GCC默认选择右到左求值,所以
ByValue(std::move(v), v.size())会先计算v.size()得到42,再处理std::move(v),最终s.x为42; - Clang默认选择左到右求值,先处理
std::move(v)掏空原对象,再计算v.size()得到0,最终s.x为0。
两种编译器的行为都符合标准,因为标准没有强制规定这个顺序。
- GCC默认选择右到左求值,所以
右值引用传参的特殊性:
当构造函数或函数按右值引用接收参数时,std::move(v)只是把原对象绑定到引用上,不会触发移动构造函数——真正的移动操作是在成员初始化列表(v(std::move(v)))或函数内部执行的,此时v.size()已经完成求值,所以不管用大括号还是小括号,s.x都会是42。
总结
- 不是GCC或Clang有“固定求值顺序”,而是普通函数/构造调用的实参顺序由编译器自主决定,两个编译器的实现策略不同;
- 大括号初始化的实参求值顺序是标准强制的左到右,这是导致
ByValue{std::move(v), v.size()}始终返回0的核心原因; - 按值传参时,传递
std::move(v)会立即触发移动构造创建形参,这才是原对象被掏空的真正原因,std::move本身只是类型转换,不会修改对象。
内容的提问来源于stack exchange,提问作者Ben
相关产品推荐
相关产品推荐

