将局部变量移动到不同类型的 return 值中

Moving local variable into return value of different type

当 return 从 C++ 中的函数获取值时,我们有复制省略和(命名)Return 值优化帮助我们创建更高效​​的代码。简而言之,如下代码:

std::vector<int> make_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v;
}

导致静默移动或直接构造到 return 值的目标,而不是复制。围绕此的规则还意味着在 returning 时显式移动 returned 对象实际上会阻止这些优化。

std::vector<int> make_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // BAD
}

此版本可防止 RVO,如 Scott Meyers 的 Effective Modern C++,第 25 项所述。


我的问题是当 return 类型不同但可以从一个或多个局部变量移动构造时会发生什么?考虑以下每个 return 一个可选向量的函数:

std::optional<std::vector<int>> make_opt_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v; // no move
}

std::optional<std::vector<int>> make_opt_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // move
}

以下哪个是正确的? return std::move(v) 一开始对我来说像是一个危险信号,但我也怀疑这是正确的做法。以下两个函数 returning 一对向量也是如此:

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_1(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {v1, v2}; // no move
}

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_2(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {std::move(v1), std::move(v2)}; // move
}

在这种情况下,尽管乍一看很奇怪,但我认为进入 return 值是更好的做法。

当类型不同时移动到 return 值更好,但是 return 值可以从被移动的局部变量构造而来,我是否正确?是我误解了 NRVO,还是还有其他一些优化在我前面?

Am I correct that it's better to move into the return value when the types differ, but the return value can be move constructed from the local variable(s) being moved from? Have I misunderstood NRVO, or is there some other optimization that is well ahead of me here?

你确实错过了一件事。即使类型不同,也会自动完成隐式移动。

[class.copy.elision] (emphasis mine)

3 In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or

  • if the operand of a throw-expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one),

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided.  — end note ]

这不取决于类型匹配,并且是在没有发生完整 (N)RVO 的情况下的后备行为。因此,通过在 make_opt_vec_2.

中显式移动,您将一无所获。

鉴于 std::move 要么是悲观的,要么完全是多余的,我认为最好 不要 在简单地 return 一个函数时这样做本地对象。

您唯一想要明确编写移动的情况是当您 return 的表达式更复杂时。在那种情况下,你确实是一个人,不动是一种潜在的悲观情绪。所以在 make_vec_pair_2 中,进入 对是 正确的做法。

这里的经验法则是不要只移动作为函数局部对象的 id-expression。否则,搬走。