将 r 值作为非常量引用传递(VS 警告 C4239)

Posted

技术标签:

【中文标题】将 r 值作为非常量引用传递(VS 警告 C4239)【英文标题】:Passing r-value as non-const reference (VS warning C4239) 【发布时间】:2013-04-25 13:26:50 【问题描述】:

我想要做的(使用 C++ lambda)实际上是:

std::vector<MyType> GetTheArray () return something;

const auto DoSomething = [](std::vector<MyType> & array)

     //Some processing that involves either sorting the 'array' or setting temporary flags on the items
;

DoSomething (GetTheArray ());

这在标准 C++ 中似乎是不允许的,因为右值不能作为非常量引用传递。

我的问题:

1) 有没有办法使用类型转换来做到这一点,还是我必须创建一个临时变量来存储 GetTheArray () 的结果?

2) C++ 不允许这样做有充分的理由吗?

请注意,从“GetTheArray”返回的“something”是动态构造的数组,而不是存储值。

【问题讨论】:

为什么要对一个在函数返回后立即超出范围的临时数组进行排序? 相关:***.com/q/4084053/951890 有一些方法可以诱使编译器在不涉及创建变量的情况下允许这样做,但正如前文所述,为什么要更改将立即死亡的容器? 2 个可能的原因:a)我需要对其进行排序以便将其与另一个数组进行比较 b)我需要在数据上设置标志,然后再将其传递给另一个函数进行处理(在我的情况下可以将其保存到数据库中) 感谢@Vaughn Cato。我可以看到该问题中有一些相关信息。 【参考方案1】:

从 cmets 看来,您想要的是获取一个向量,对其进行破坏性修改(在无法重置原始状态的意义上),然后在内部使用结果。并且您希望这对左值和右值都有效。

下一个问题是在左值的情况下,保存原始容器的代码在函数调用完成后是否需要它,以及它是否需要向量的原始状态。根据答案,您有不同的解决方案:

持有左值的调用者在调用后不再使用它

(或者,持有左值的调用者需要原始状态)

这是最简单的情况。然后你的函数应该按值获取容器。如果调用者有一个左值,它可以std::move 避免复制(它不再关心对象)或 copy 这可能更昂贵但保持原始容器不变。

如果使用 rvalue 调用函数,则副本将被省略或转换为廉价的隐式移动。

持有左值的调用者不需要原始状态,但需要容器

这种情况比较困难,您需要为同一个函数(或 lambda)提供两个重载,一个采用 lvalue 供此调用者使用,另一个采用 rvalue-reference 用于调用者手临时的情况。在这两种情况下,绑定都很便宜。虽然这需要更多代码,但您可以根据另一个重载实现一个重载:

rtype f(std::vector<Data> & );   // for lvalues
rtype f(std::vector<Data> && v)  // for rvalues
    return f(v);               // v is an lvalue here

您正在执行 lambdas 的事实可能会使这稍微复杂一些,但希望不会太多。

【讨论】:

非常有趣。是的,我通过更改我的 lambda 签名以通过 r 值引用获取数组来获得干净的编译。在您看来,这是一个“好的”解决方案,还是应该为移动构造函数和移动赋值运算符保留通过 r 值引用传递参数? @Coder_Dan:当您需要区分左值和右值时,您应该只在界面上使用右值引用。这不仅限于构造函数/赋值,还包括您需要区分的任何操作。 @Coder_Dan:不确定是否足够清楚,如果您仅提供右值引用重载,它将不接受 lvalue 作为参数,因此您限制了采用。如果您开始考虑对于 lvalues 调用者执行std::move 很好,那么您不应该通过 rvalue-reference 获取参数,而是通过 value . 罗德里格斯 - 感谢您的提示。我想我跟着你,看看如果这个函数在公共接口中,或者我也需要接受一个左值引用,那么最好按值传递参数。您还告诉我,在这种情况下,如果我传递包装在对 std::move 的调用中的参数,我可以在传递左值时避免向量的副本,这是一个我以前没有考虑过的有趣概念。在我的例子中,它是一个函数局部 lambda,每次都以相同的方式调用,并为“GetTheArray”函数提供不同的参数。【参考方案2】:

如果您出于某种原因确实想修改 something,您可以随时将临时文件复制到您的 lambda 中。

const auto DoSomething = [](std::vector<MyType> array)  /*whatever*/ 

您将 lambda 称为编译器的方式可能能够省略副本。 C++11 移动语义对此进行了形式化。

【讨论】:

是的,你是对的。在这种情况下,按值传递“数组”应该没有开销——很明显(嗯,应该是这样)。我想这回答了我的具体问题,尽管可能不适用于更一般的情况。 但是如果你用左值调用会有一些开销,因为它会复制容器。根据这是否重要(或者持有左值的调用者是否不再关心它),您可以采取不同的道路......【参考方案3】:

(实际上,@honk 在按值传递方面击败了我。不同的是,我将扩展我的答案以明确使用&amp;&amp; 进行讨论)

删除&amp;,直接按值传递。

您使用的是 lambda,因此您使用的是 c++11,因此您应该只通过 value 传递它。是的,它会一样快。

C++11 非常善于发现临时文件何时被“复制”。它将用 move 替换副本。然后它会很快,并且可以说会更容易阅读。

因此,即使您可能对&amp;&amp;std::move 等一无所知,但可以肯定地说,您应该开始转向通过值而不是通过引用传递事物。它将导致代码更具可读性,如果在正确的时间使用,它不应该导致速度变慢。

基本上,如果您将临时值(例如函数返回的值)传递给函数会很快,但前提是您使用的是标准容器,例如vector。否则,您需要编写自己的 &amp;&amp; 感知构造函数以利用可能的加速。

更新:如果您决定将其强制为引用,并且/或者您正在使用自定义容器,那么您可以将 &amp; 替换为 &amp;&amp;。这将允许您传递非常量引用。这称为右值引用

正如许多其他人所指出的,临时文件将在DoSomething 返回后立即销毁。请记住这一点。这意味着您将希望在数组返回之前完全使用它的内容。也许您将其打印到屏幕上,或者将其存储在数据库中。或者,您可以将数据复制或移动到另一个对象中并返回。

【讨论】:

【参考方案4】:

您正在处理从您的底线中的GetTheArray() 调用返回的临时结果。这只能绑定到一个 const 引用,然后对象的生命周期将延长到它所绑定的 const 引用的生命周期(因此是“绑定”)。

即使你可以做你想做的事,你打电话给DoSomething 仍然是一个昂贵的无操作。至少你需要return 来自DoSomething 的东西。

C++ 禁止这样做,因为(自 C++11 起)它具有带有移动语义的右值引用,可以做一些很棒的事情,但不能不返回 something

【讨论】:

C++ 早在 C++11 之前就禁止这样做了,所以最后一句写出来的意义不大。 rvalue-references(2011 年新增)不能成为 1998 年不允许这样做的原因,除非您相信标准机构的 反因果关系 好吧,至少有两个主要的实现实现了一个扩展(MSVC 和 Intel 在 Windows 上与 MSVC 兼容),它是有用的。另一方面,回答“为什么禁止”的问题主要是解决鸡和蛋的问题。我想在这种情况下@alexrider 是正确的,它是为了防止像 OP 这样的无操作。 在标准被写下之前,有两个实现允许这样做,Solaris 和 VS。然后他们坚持支持这一点或破坏兼容性。英特尔编译器专门模仿 Windows 中的 VS 和 linux 上的 gcc 的行为,因此在他们的情况下,再次需要:能够将编译器用作本机编译器的替代品。尽管如此,rvalue-references 不能成为在它们成为标准之前的 13 年不允许的原因。 (至少在我简单的头脑中,时间旅行不存在,因果关系统治着世界) 感谢@David Rodriguez。这很有趣。【参考方案5】:

如果您需要修改something 数组,您需要返回对它的引用。否则不要通过(非const)引用DoSomething 不允许它的原因是为了避免您在此处说明的问题。看起来您要修改 something,但 GetTheArray() 返回的 something 的副本将被更改。这没有多大意义,因为此副本将在 DoSomething 退出后立即删除。 您有 3 种方法可以解决此问题。 1.需要修改something再修改GetTheArray返回引用 2.您需要修改将被丢弃的something 的副本DoSomething 完成它的工作,然后更改DoSomething 以按值接受数组。 3.DoSomething中的something不需要修改,那么const引用作为DoSomething的参数应该使用。

【讨论】:

谢谢@alexrider - 是的,返回的“东西”实际上是一个动态构建并返回的数组。我想我应该更清楚一点。我明白你在做什么,如果没有注意到一个简单的访问器没有通过引用返回值,可能会出现混淆(错误)。

以上是关于将 r 值作为非常量引用传递(VS 警告 C4239)的主要内容,如果未能解决你的问题,请参考以下文章

将 opencv Mat 传递给函数时,对非常量的引用的初始值必须是左值

返回 std::pair 与通过非常量引用传递

是否有简单的 PHP 代码来区分“将对象作为引用传递”与“将对象引用作为值传递”?

值传递vs引用传递

允许将右值绑定到非常量左值引用吗?

非常量引用的初始值必须为左值的问题