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

Posted

技术标签:

【中文标题】return 语句中使用的局部变量不会隐式转换为 r 值以匹配转换运算符【英文标题】:The local variable which is used in return statement doesn't convert to r-value implicitly to match the conversion operator 【发布时间】:2021-03-24 03:30:52 【问题描述】:

在下面的示例 sn-p 代码中,return 语句中使用的局部变量不会隐式转换为右值以匹配 转换运算符。但是对于 move 构造函数,它可以工作。

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

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

#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;

【问题讨论】:

来自a little test,看来clang比gcc更严格。我自己预计return x; 会出错,因为x 在这种情况下是一个左值。 (当然,我不是标准。) 看起来 C++11 标准只允许使用构造函数进行隐式移动(疏忽?),但当前草案允许它用于转换运算符(而 Clang 和 cppreference 就在次)。 @TrebledJ 为什么你期望return x;g() 中出现错误,但在f() 中却没有?有什么区别?我希望它们都是正确的;因为在return 声明之后,它们不再需要它们,并且移出它们更有效。 f() 中的 return x; 不同,因为可能会发生复制省略,从而优化复制/移动。然而,在g() 中,x 是一个左值(而std::move(x)X 是右值)并且没有定义operator std::string() &amp;,所以我的直觉是它会出错。就像我说的,我的直觉不一定正确。 标准规定在return x;x 的类型与函数的返回类型匹配并且x 是一个局部变量,执行重载决议就好像x 是一个右值,从而允许找到移动构造函数。 g() 不匹配那个特殊情况,因为返回类型不匹配 x 【参考方案1】:

f 在C++11 standard 下工作的原因(链接是足够接近的草稿)是这个条款

[class.copy]/32

当满足或将满足省略复制操作的标准时,除了源 object 是函数参数,要复制的对象由左值指定,重载决议为 首先执行选择复制的构造函数,就好像对象是由右值指定的一样。 ...

在这种情况下,“复制操作省略的标准[on]”是

[class.copy]/31.1

在具有类返回类型的函数中的 return 语句中,当表达式是具有相同 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时函数返回类型,直接在函数返回值中构造自动对象即可省略复制/移动操作

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

我认为首先了解为什么这条规则可能很重要。这条规则真的不是关于将函数局部变量隐式移动到函数返回值中,尽管它确实是这么说的。这是关于使 NRVO 成为可能。考虑一下如果没有这些规则,你必须为f 写什么:

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

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

X f() 
    X x;
    return x;

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

你看,[class.copy]/32 确实没有g 工作。它在f 中的目的是使执行zero 复制/移动构造函数成为可能。但是g来执行转换操作符;当你给它一个X 时,语言没有其他明智的方法来提取std::string。所以NVRO不能申请g,所以不用写return x;,直接写就行了

std::string g() 
    X x;
    return std::move(x);

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

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

[class.copy.elision]/3

隐式可移动实体是一个自动存储持续时间的变量,它可以是非易失性对象,也可以是对非易失性对象类型的右值引用。在以下复制初始化上下文中,在尝试复制操作之前首先考虑移动操作:

如果return ([stmt.return]) 或co_­return ([stmt.return.coroutine]) 语句中的表达式 是一个(可能带括号的)id-expression命名在最内层封闭函数或 lambda-expression 的主体或 parameter-declaration-clause 中声明的隐式可移动实体,或 ...

选择用于复制的构造函数的重载解析或要调用的return_­value 重载首先执行,就好像表达式或操作数是右值一样。 ...

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

【讨论】:

奇怪的是,当我在您的链接代码中将int 更改为string 时,gcc 和msvc(不是c++latest)可以编译代码:godbolt.org/z/GezKoc

以上是关于return 语句中使用的局部变量不会隐式转换为 r 值以匹配转换运算符的主要内容,如果未能解决你的问题,请参考以下文章

我应该*总是*支持 C# 3.0 中的隐式类型局部变量吗?

C# 3.0 LINQ的准备工作

何时会发生隐式类型转换

JS强制类型转换,隐式类型转换, == 和===的区别

类型转换

异常捕获try----catch