返回复合类型时,`std::move` 对象进入构造函数?

`std::move` objects into constructor when returning composite type?

我有一个 class,它有几个(可能很大)std::vector<T> 成员。据我所知,这不算坏风格。简化示例:

// C++17
#include<vector>

struct ThreeVectors {
    std::vector<int> v1;
    std::vector<int> v2;
    std::vector<int> v3;
};

然后我还有一个函数,它根据参数计算一些 std::vector<int> 并将它们 returns 包装到 ThreeVectors 对象中:

ThreeVectors no_move(const std::vector<int> &vv1, // copy manually in the function
                     std::vector<int> vv2,  // copy when passing argument
                     const std::vector<int> &vv3 // copy when wrapping into ThreeVectors
) {
    auto modvv = vv1;  // make a copy
    // perform lots of manipulations, including changing size (e.g. filtering).
    modvv.at(0)++;

    vv2.at(0)++;  // perform lots of manipulations

    // do nothing with vv3.

    return ThreeVectors{modvv, vv2, vv3};
}

示例用户代码:

void user() {
    std::vector<int> v1;
    std::vector<int> v2;
    std::vector<int> v3;
    for (int i = 0; i < 10'000'000; ++i) {
        v1.push_back(i % 10'000);
        v2.push_back(i % 11'000 + 30);
        v3.push_back(i % 12'000 + 60);
    }

    auto wrapped = no_move(v1, v2, v3);
    // no longer need v2. Could std::move it if that helps?
    // still need v1 and v3. Cannot avoid one copy but avoid more?
    // ...
}

问题:我是否应该在任何地方使用移动操作来提高效率?

特别是,以下问题可能很有趣:

  1. user 可能正在使用 user 不再需要的向量调用 no_move。将它移动到函数中是否有意义(这仅对函数不接受 const& 的参数有意义)?
  2. 如果函数无论如何都必须制作副本,它是否应该永远不接受 const&
  3. no_move 是否应该将内容移动到 ThreeVectors 的构造函数中?
  4. no_move 不修改其最后一个参数而仅将其包装这一事实对如何处理该参数有任何影响吗?

我尝试分析可以将 std::move 与 quick-bench.com 放在一起的不同位置组合,但我不断收到运行时错误(这可能意味着我使用 std::move 不正确) .而不是用所有不同的组合来发送我的问题,所有这些组合都编译但其中许多在运行时崩溃,我要求最佳实践解决方案。

你是对的,规则是当你无论如何都要复制时,采用 const& 会适得其反。唯一能做的就是避免复制。取取值,从no_move里面的值移动,然后用户代码可以决定参数是否可以移动或者是否必须复制。不,vv3 被转发但 vv1 被操纵的事实并不意味着他们受到不同的对待。唯一重要的是该函数正在“取得”两者的所有权。

另外,请注意 ThreeVectors 没有构造函数。它通过聚合初始化来初始化,它初始化每个字段,就好像通过 field(initializer);(复制初始化)一样。所以你最好把 std::moves 放在那里。

ThreeVectors no_move(std::vector<int> vv1, // copy OR move from user
                     std::vector<int> vv2, // copy OR move from user
                     std::vector<int> vv3  // copy OR move from user
) {
    vv1.at(0)++;
    vv2.at(0)++;
    return {std::move(vv1), std::move(vv2), std::move(vv3)};
}

请注意,做对比做错简单

void user() {
    std::vector<int> v1, v2, v3;
    for (int i = 0; i < 10'000'000; ++i) {
        v1.push_back(i % 10'000);
        v2.push_back(i % 11'000 + 30);
        v3.push_back(i % 12'000 + 60);
    }
    auto wrapped = no_move(v1, std::move(v2), v3);
    // say what you mean:
    //  1. v1 and v3 must remain under user's ownership and no_move must
    //     receive copies, since it also wants ownership
    //  2. user no longer needs v2 and it can just be given to no_move
}

是的,你应该。

针对您的问题:

  1. 是的,move当您不再需要它时,它不会被复制
  2. 是的,除非它有可以避免复制的快速路径。按价值接收创造了接收可以廉价移动构造的 r 值的可能性;收到 const 参考排除了。
  3. 是的。
  4. 通过 const 引用接收是有争议的。如果您按值接收,并且 std::moveThreeVectors,您保证一次移动构造,再加上一次移动或一次复制构造。如果您通过 const 引用接收,您总是进行复制构造,但根本没有移动构造。对于 std::vector,我可能会按值和 std::move 接收到 ThreeVector。或者,您可以通过 r 值引用 (&&) 接收它,但这会稍微限制使用。

所以我的建议是:

  1. 函数本身应该按值接收 vv1;没有理由通过 const 引用接收,因为它将在内部复制而不通过引用使用
  2. return ThreeVectors{modvv, vv2, vv3};应该是return ThreeVectors{std::move(modvv), std::move(vv2), vv3};; (vv1 而不是 modvv 如果你按照建议的值接收它)复制省略可以处理不复制 ThreeVectors 实例本身(它被返回并且 RVO 应用),但弄清楚如何省略复制到新 ThreeVectors 很难,而且不太可能发生。
  3. 调用函数时,用auto wrapped = no_move(v1, std::move(v2), v3);;如果您遵循#1 中的建议,这将从 v1 复制构造(效率低但根据需要保留未修改),从 v2 移动构造(高效)并接收未修改的 v3参考。没有std::movev2会被复制,浪费内存和时间。
  4. 可选地,按值接收 v3 也可能有意义,因为在需要副本时添加移动的增量成本相对于在复制时仅使用两个移动的好处而言很小可以避免。

如果我写 API,我会这样做:

ThreeVectors no_move(std::vector<int> vv1,
                     std::vector<int> vv2,
                     std::vector<int> vv3
) {
    // perform lots of manipulations, including changing size (e.g. filtering).
    vv1.at(0)++;

    vv2.at(0)++;  // perform lots of manipulations

    // do nothing with vv3.

    return {std::move(vv1), std::move(vv2), std::move(vv3)};
}

您的具体用途是:

auto wrapped = no_move(v1, std::move(v2), v3);

涉及两个复制构造和四个移动构造(简单地战略性地添加 std::move 但否则保持原始代码不变将有两个复制构造和三个移动构造),但其他调用者可能会调用它有:

auto wrapped = no_move(std::move(v1), std::move(v2), std::move(v3));

从而在不需要保留调用者的 vector 时避免 所有 复制构造开销(仅六个移动构造)。