return 语句中使用的局部变量不会隐式转换为右值以匹配转换运算符

The local variable which is used in return statement doesn't convert to r-value implicitly to match the conversion operator

在下面的示例代码片段中,return 语句中使用的局部变量不会隐式转换为 r 值以匹配 转换运算符 。但是对于 move constructor 它有效。

我想知道这是标准行为还是错误。如果是标准行为,原因是什么?

我在 Microsoft Visual Studio 2019(版本 16.8.3)中以 'permissive-' 模式对其进行了测试,它产生了编译器错误。但是在'permissive'模式下,就OK了。

#include <string>

class X
{
    std::string m_str;
public:
    X() = default;
    X(X&& that)
    {
        m_str = std::move(that.m_str);
    }
    operator std::string() &&
    {
        return std::move(m_str);
    }
};

X f()
{
    X x;
    return x;
}

std::string g()
{
    X x;
    return x; // Conformance mode: Yes (/permissive-) ==> error C2440: 'return': cannot convert from 'X' to 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
    //return std::move(x); // OK
    // return X{}; // OK
}

int main()
{
    f();
    g();
    return 0;
}

fC++11 standard 下工作的原因(link 是一个足够接近的草稿)是这个条款

[class.copy]/32

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. ...

在这种情况下相关的“省略复制操作的条件”是

[class.copy]/31.1

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

这适用于 f,因为 return x 中的 x 是“非易失性自动对象的名称......与函数具有相同的 cv-unqualified 类型return类型";该类型是 X。这不适用于 g,因为 return 类型 std::string 不是 x.

命名对象的类型 X

我认为首先理解为什么这条规则可能很重要。这条规则并不是 真正 关于将函数局部变量隐式移动到函数 return 值,即使这就是它的字面意思。这是关于使 NRVO 成为可能。考虑一下如果没有这些规则,您将不得不为 f 编写什么:

X f() {
    X x;
    return std::move(x);
}

但是 NVRO 无法应用,因为您没有 returning 变量;您正在 return 函数调用的结果!所以子句 [class.copy]/32 是关于让你的代码

X f() {
    X x;
    return x;
}

语法上合法,而子句描述的语义(使用移动构造函数)将被忽略(假设您的实现不太愚蠢)因为我们实际上 只是要做 NRVO,它不会调用任何东西。

你看,真的,[class.copy]/32 没有 g 工作。它在 f 中的目的是使执行 zero copy/move 构造函数成为可能。但是g执行转换运算符;当你给它一个 X 时,语言没有其他明智的方法来拉出 std::string。所以NVRO不能在g申请,所以没必要写return x;,直接写

就可以了
std::string g() {
    X x;
    return std::move(x);
}

不用担心会导致错过优化。

我们看到 C++11 规则 [class.copy]/32 的设计使其影响可能的最小部分情况。它适用于我们 喜欢 NVRO 但没有复制构造函数的情况,并通过告诉我们假装调用移动构造函数来使 NVRO 成为可能。但是当实际 编写 代码时,这意味着要记住一条令人费解的规则:“要最小化 copies/moves,如果 return 类型与变量的类型 return the_variable;,否则 return std::move(the_variable)。”这就是为什么 C++20 标准将 [class.copy]/32 完全改写为

[class.copy.elision]/3

An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:

  • If the expression in a return ([stmt.return]) or co_­return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
  • ...

overload resolution to select the constructor for the copy or the return_­value overload to call is first performed as if the expression or operand were an rvalue. ...

要求return类型与隐式移动的变量类型相同;它可以概括为概念上更简单的规则“returning 变量尝试移动,然后尝试复制”。这导致了概念上更简单的原则“当 return 从函数中获取变量时,只需 return the_variable; 它就会做正确的事情”。 (当然,none of GCC, Clang, or MSVC seem to have gotten the memo。那一定是某种记录...)