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() &
,所以我的直觉是它会出错。就像我说的,我的直觉不一定正确。
标准规定在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 值以匹配转换运算符的主要内容,如果未能解决你的问题,请参考以下文章