返回值的转发。需要使用std::forward吗?

37

我正在编写一个库,它将来自其他库的许多函数和方法进行包装。为了避免复制返回值,我使用std::forward进行应用,如下所示:

template<class T>
T&& wrapper(T&& t) { 
   f(t);  // t passed as lvalue  
   return std::forward<T>(t);
}

f返回void并使用T&&(或以值为重载)。包装器始终返回包装器的参数,并且在返回值上应保留参数的值。在return中实际上需要使用std::forward吗?RVO是否使其变得多余?事实上,它是一个引用(R或L)是否使其变得多余?如果返回不是最后一个函数语句(在某些if内部),是否需要?

是否应该将wrapper()返回voidT&&存在争议,因为调用者可以通过参数(引用,R或L)访问评估的值。但在我的情况下,我需要返回值,以便可以在表达式中使用wrapper()

这可能与问题无关,但已知函数f不从t中窃取,因此f(std::forward<T>(t))中第一次使用std::forward是多余的,我已将其删除。

我编写了一个小测试:https://gist.github.com/3910503

测试显示,在gcc48和clang32中使用-O3未转发的T会创建额外的副本(RVO不会启动)。

此外,我无法从UB中获得不良行为:

auto&& tmp = wrapper(42); 

这并不能证明任何事情,因为它是未定义的行为(如果确实是UB的话)。


3
我发誓,如果我再见到有人把 && 叫做“通用引用”,我就... - Nicol Bolas
2
什么是“通用引用”? - GManNickG
3
@LeonidVolnitsky:术语“通用引用”不是C++的正确术语;在C++规范中找不到它。目前,我听说过这个术语的唯一地方是Scott Meyers的演讲。我真的希望它不会流行起来。 - Nicol Bolas
1
@Nicol: 来自微软的Stephan T Lavavej也经常使用这个术语,鉴于他在Channel9上的大量视频,无论你喜不喜欢这个术语,它都越来越流行了。 - ildjarn
2
@NicolBolas:这个习惯用语的命名比RAII好多了。而且,我听说“转发引用”即将取代它。 - Quentin
显示剩余6条评论
3个回答

13

如果您确信在调用ft不会处于已移动状态,则有两种比较合理的选择:

  • 返回类型为T&&std::forward<T>(t),避免了任何构造,但允许编写例如auto&& ref = wrapper(42);的代码,这将导致ref成为悬空引用。

  • 返回类型为Tstd::forward<T>(t),最坏情况下在参数为rvalue时请求移动构造——这避免了prvalues的上述问题,但可能从xvalues中窃取。

在所有情况下都需要使用std::forward。由于t始终是引用,因此不考虑拷贝省略。


auto&& ref = wrapper(42) 会留下悬垂引用吗?我以为临时变量的生命周期会延长到 ref 的作用域(请参见 https://dev59.com/J4zda4cB1Zd3GeqPqsIQ 上的第一个评论)。 - Claudiu
@Claudiu,你提到的第一个注释展示了一个扩展临时变量的例子,即在auto&& tmp = id(5);中。在这里,从5创建的临时变量只存在到分号结束。这就是我所警告的危险、不明显的行为。 - Luc Danton
1
@Claudiu 如果一个临时对象被引用绑定(即通过值返回),那么它的生命周期将会延长。如果函数返回任何类型的引用,即使是右值引用,它也不是一个临时对象。 - ABu

8
根据传递给此函数的内容,其结果可能是未定义的行为!更确切地说,如果将非左值(即右值)传递给此函数,则返回引用所引用的值将过时。同时,T&&并不是“通用引用”,尽管其效果有些类似于通用引用,因为T可以被推导为T&T const&。问题出现在当它被推导为T时:参数以临时方式传递,并在函数返回之后但在任何东西获取到对其的引用之前就会消失。
使用std::forward<T>(x)的用途仅限于在调用另一个函数时转发对象:作为临时变量输入的对象在函数内部看起来像是左值。使用std::forward<T>(x)使得如果x是临时对象,则x看起来像是临时对象- 因此,在创建被调用函数的参数时允许从x移动。
当从函数返回对象时,您可能需要处理一些情况,但其中没有一个涉及std::forward()
  • 如果类型实际上是引用(const或非const),则不需要对对象进行任何操作,只需返回引用。
  • 如果所有的return语句都使用相同的变量或都使用一个临时变量,则可以使用复制/移动省略,并且在合适的编译器上将被使用。由于复制/移动省略是一种优化,因此不一定会发生。
  • 如果始终返回相同的局部变量或临时变量,则可以从中移动(如果有移动构造函数),否则对象将被复制。
  • 当返回不同的变量或涉及表达式时,仍然可以返回引用,但是复制/移动省略将不起作用,如果结果不是临时的,则直接移动将不可能。在这些情况下,需要使用std::move()来允许从局部对象移动。
在大多数情况下,生成的类型是T,您应该返回T而不是T&&。但是,如果T是左值类型,则结果可能不是左值类型,因此可能需要从返回类型中删除引用资格。在您特别询问的场景中,类型T适用。

Prvalues可能会有问题,确实会导致过时的引用,但xvalues不太可能出现UB -- 尽管我发现返回一个已移动的值并不特别有用。 - Luc Danton
3
如果您将一个右值传递给这个函数,那么所返回引用所引用的值将会过期。或许我有些迟钝,但是这是为什么呢?如果调用者将一个右值传递给一个临时对象,那么这个临时对象是否至少会在调用者的完整表达式结束之前一直存在,因此足够被此函数的返回值引用?例如my_Foo_vector.emplace_back(wrapper(Foo());。我知道返回T也是可以的,只是不明白何时返回T&&会产生过期引用。 - Steve Jessop
你创建的临时对象,在概念上并不是函数所看到的对象!函数在概念上看到的是该对象的副本。参数的生命周期比创建临时对象的完整表达式要短。特别地,5.2.2 [expr.call]第4段包含以下句子:“……当定义它的函数返回时,参数的生存期结束……”实际上,你会看到该对象的复制/移动被省略了。即使我理解有误,从概念上讲,传递对象时它也已经过时了,因为它正在被移动。 - Dietmar Kühl
实际上,我觉得我已经把自己搞糊涂了:右值引用仍然是一个引用,即临时变量不应该有任何副本,并且它的寿命足够长,可以返回对它的引用。话虽如此,问题仍然存在,但比我想象的更深入:在调用 f(std::forward<T>(t)) 时,函数 f() 获得了从 t 移动对象的权限,并且在 wrapper() 中引用 t 需要假设 t 已经被移动。如果 f() 没有从传递的对象中移动,调用 f() 时就没有必要使用 std::forward<T>() - Dietmar Kühl
好的,慢慢地理解了:如果t的内容无法被窃取,则调用f()时不应将std::forward<T>(t)传递给被调用函数:转发的目的是使内容可以被窃取。另一方面,当将t作为T&&返回时,有必要恢复t的右值性,即使用std::forward<T>(t)来提供T&&的返回类型才能得到正确的返回值。话虽如此,我认为接口仍然太脆弱了。 - Dietmar Kühl
+1,感谢您非常详细的回答。在我的特定情况下,包装器始终返回包装器的参数,并且返回值应保留参数的值。 - Leonid Volnitsky

3
不,你不需要使用std::forward。最好根本不返回r-value引用,因为这会阻止NRVO优化。你可以在这篇文章中了解更多关于移动语义的知识:文章

5
(N)RVO 不能应用于函数参数,只能应用于具有自动存储期的本地变量。 - ildjarn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接