You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

移动语义与大括号初始化下构造函数参数的求值顺序问题

构造函数按值传参时的求值顺序陷阱

我们都知道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

问题解析

  1. 大括号初始化的求值顺序:
    C++标准明确规定,列表初始化(大括号形式)的实参求值顺序是严格从左到右。所以T{std::move(v), v.size()}会先处理std::move(v)——由于ByValue的构造函数按值接收参数,这里会触发std::vector的移动构造函数,直接掏空原v;之后再调用v.size()自然返回0,这是符合标准的行为。

  2. 普通函数/构造调用的求值顺序:
    对于小括号形式的构造或普通函数调用,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。
      两种编译器的行为都符合标准,因为标准没有强制规定这个顺序。
  3. 右值引用传参的特殊性:
    当构造函数或函数按右值引用接收参数时,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 11:15:33