尽管按名称返回,为什么局部变量仍被复制? [class.copy.elision] (C++20)

问题描述

在以下代码段中,return s 给出警告 local variable 's' will be copied despite being returned by name [-Wreturn-std-move]。为什么会这样?

我使用这个 lambda 函数的目标是获取输入字符串的所有权,然后在通过 RVO 或移动语义修改后返回它。我真的很想避免任何复制。

const auto to_upper = [](std::string&& s) {
    std::transform(s.begin(),s.end(),s.begin(),[](unsigned char c){ return std::toupper(c); }
    );
    return s;
};

返回 std::move(s)std::forward<std::string>(s)解决问题,但我认为这没有必要,因为编译器可以省略复制构造函数的使用。另外,我认为我应该使用 std::forward 但哪个是正确的,为什么?

解决方法

在以下代码段中,return s 给出警告 local variable 's' will be copied despite being returned by name [-Wreturn-std-move]。为什么会这样?

那是因为自动推导出函数的返回值类型或 lambda 捕获调用运算符永远不是引用。见Return type deduction

如果返回类型不使用decltype(auto),则推导遵循模板参数推导规则。

Template argument deduction 从不推断引用。

如果需要引用返回值,则必须明确指定:

const auto to_upper = [](std::string&& s) -> std::string&& {
    // ...
    return std::move(s);
}

std::move(s) 修复了编译器警告,但它不会更改返回值类型,除非明确指定引用返回类型。 An example。编译器警告已损坏。


当函数按值返回时,函数参数免于返回值复制省略。

请参阅 copy elision 了解完整详情:

return 语句中,当操作数是具有自动存储期的非易失性对象的名称时,它不是函数参数或 catch 子句参数,并且它与函数返回类型具有相同的类类型(忽略 cv 限定)。这种复制省略的变体被称为 NRVO,“命名返回值优化”。

class.copy.elision

在具有类返回类型的函数中的 return 语句中,当表达式是具有自动存储期的非易失性对象的名称时(函数参数或由与函数返回类型具有相同类型(忽略 cv 限定)的处理程序 ([except.handle])) 的异常声明,可以通过将对象直接构造到函数调用的返回对象

函数参数和返回值必须同时存在。构造返回值时,局部变量仍然存在,并且可能引用函数参数。只有在构造返回值之后,才会执行局部变量的析构函数。那些析构函数可能会引用函数参数,这就是为什么它的存储不能用于之前构造的返回值。

,

(N)RVO 仅当在被调用函数中创建的局部变量可以别名为返回内存时才有效。

参数 s 是一个引用。该函数正在获取一个传递给它的地址,该地址的实际值位于该函数无法控制的其他位置。

为了工作,请将参数 s 定义为一个值。

另外,请注意 (N)RVO、移动语义和 C++17 的新复制省略的概念都是不同的东西。如果字符串数据是与字符串的结构分开分配的,那么它们中的任何一个都将避免制作字符串数据的副本(但无论如何 SSO 都会阻止这种情况)。中间部分仍然复制结构本身,其中将包括指针或 SSO 内部缓冲区,但如果函数被内联,这些原语本身可能会在较低级别上进行优化。

,

有一个规则允许局部变量(包括函数参数)在返回时隐式移动(而不是复制),如果函数按值返回,并且返回的变量与返回类型具有相同的类型。

>

警告告诉您在这种情况下不会发生这种隐式移动,因为 s 是一个引用并且该规则不适用于引用。


我想我应该使用 std::forward

不,您应该使用 std::move,因为您的参数不是转发引用。 T && 仅在 T 是模板参数(或 auto)时才被视为转发引用,这是在进行调用时推断出来的。


似乎规则在 C++20 中发生了变化,现在也可以隐式移动右值引用。从 C++20 开始,此处的 std::move 可以删除。

这是由:

[class.copy.elision] (C++20)

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

(3.1) — 如果 return ([stmt.return]) 或 co_return ([stmt.return.coroutine]) 语句中的表达式是一个(可能带括号的)id 表达式,它命名在最内部封闭函数或 lambda 表达式的主体或参数声明子句,或 ...

C++20 之前的措辞不同,它只允许“自动对象”(不允许引用)。