std::forward 的主要目的是什么,它解决了哪些问题?

What are the main purposes of std::forward and which problems does it solve?

提问人:Steveng 提问时间:8/27/2010 最后编辑:user3100212Steveng 更新时间:11/24/2022 访问量:116889

问:

在完全转发中,用于将已命名的右值引用转换为未命名的右值引用。这样做的目的是什么?如果我们将 & 保留为左值,这将如何影响被调用的函数?std::forwardt1t2innert1t2

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
C ++11 右值引用 C++-FAQ 完美转发

评论

0赞 user202729 1/22/2022
备注:也可以写 或者 ,看 c++ - 在 lambda 中完美转发? - 堆栈溢出std::forward<decltype(t1)>(t1)decltype(t1)(t1)
0赞 Matthieu 11/21/2023
可能的原型是什么?inner()

答:

13赞 sbi 8/27/2010 #1

如果我们将 t1 和 t2 保留为 lvalue,这将如何影响被调用的函数 inner?

如果在实例化后,其类型为 ,并且属于类,则要传递每个副本和每个引用。好吧,除非根据非参考来获取它们,也就是说,在这种情况下,您也想这样做。T1charT2t1t2constinner()const

尝试编写一组函数,这些函数在没有右值引用的情况下实现这一点,从而推断出从 的类型传递参数的正确方法。我认为你需要 2^2 个的东西,相当大的模板元东西来推断论点,并且需要很多时间来解决这个问题。outer()inner()

然后有人提出了一个每个指针接受参数的人。我认为现在是 3^2。(或 4^2。见鬼,我懒得去想指针是否会有所作为。inner()const

然后想象一下,你想对五个参数这样做。或者七个。

现在你知道为什么一些聪明的人想出了“完美转发”:它让编译器为你完成这一切。

1003赞 GManNickG 8/27/2010 #2

您必须了解转发问题。您可以详细阅读整个问题,但我会进行总结。

基本上,给定表达式 ,我们希望表达式是等价的。在 C++03 中,这是不可能的。有很多尝试,但在某些方面都失败了。E(a, b, ... , c)f(a, b, ... , c)


最简单的方法是使用 lvalue-reference:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

但这无法处理临时值 (rvalues): ,因为这些值不能绑定到 lvalue-reference。f(1, 2, 3);

下一次尝试可能是:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

这解决了上述问题,因为“const X& 绑定到所有内容”,包括左值和右值,但这会导致一个新问题。它现在不允许有非参数:Econst

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); 
f(i, j, k); // oops! E cannot modify these

第三次尝试接受 const-references,但随后是 away:const_castconst

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

这接受所有值,可以传递所有值,但可能导致未定义的行为:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); 
f(i, j, k); // ouch! E can modify a const object!

最终的解决方案可以正确地处理所有事情......以无法维护为代价。提供 的重载,以及 const 和非 const 的所有组合:f

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N 个参数需要 2 个N 个组合,简直是一场噩梦。我们希望自动执行此操作。

(这实际上是我们在 C++11 中让编译器为我们做的事情。


在 C++11 中,我们有机会解决这个问题。一种解决方案修改了现有类型的模板推导规则,但这可能会破坏大量代码。所以我们必须找到另一种方法。

解决方案是改用新添加的 rvalue-references;我们可以在推断右值引用类型时引入新规则,并创建任何所需的结果。毕竟,我们现在不可能破解代码。

如果给定对引用的引用(注意引用是一个包含术语,表示 和 ),我们使用以下规则来找出结果类型:T&T&&

“[给定] 一个类型 TR 是对类型 T 的引用,尝试创建类型”对 cv TR 的左值引用“会创建类型”对 T 的左值引用“,而尝试创建类型”对 cv TR 的右值引用“会创建类型 TR。

或以表格形式:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

接下来,使用模板参数推导:如果参数是左值 A,我们为模板参数提供对 A 的左值引用。 否则,我们正常推导。这给出了所谓的通用引用(术语转发引用现在是官方引用)。

为什么这很有用?因为组合我们保持了跟踪类型的值类别的能力:如果它是一个左值,我们有一个左值引用参数,否则我们有一个右值引用参数。

在代码中:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

最后一件事是“转发”变量的值类别。请记住,一旦进入函数,参数就可以作为左值传递给任何内容:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

那不好。E 需要获得与我们得到的相同类型的值类别!解决方案是这样的:

static_cast<T&&>(x);

这有什么作用?假设我们在函数内部,并且已经传递了一个左值。这意味着是 ,因此静态转换的目标类型是 ,或只是 。由于已经是 ,我们什么都不做,只剩下左值引用。deduceTA&A& &&A&xA&

当我们被传递一个右值时,是 ,所以静态强制转换的目标类型是 。强制转换将生成右值表达式,该表达式不能再传递给左值引用。我们保留了参数的值类别。TAA&&

把这些放在一起,我们就可以“完美转发”:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

当接收到左值时,获取左值。当接收到右值时,获取右值。完善。fEfE


当然,我们想摆脱丑陋的东西。 记忆起来晦涩难懂;让我们改为创建一个名为 的实用程序函数,它执行相同的操作:static_cast<T&&>forward

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

评论

1赞 Michael Foukarakis 8/27/2010
不会是一个函数,而不是一个表达式?f
1赞 Johannes Schaub - litb 8/27/2010
关于问题陈述,您的最后一次尝试是不正确的:它会将 const 值转发为非常量值,因此根本不转发。另请注意,在第一次尝试中,将接受:被推导为 。失败是针对右值文本的。另请注意,对于对 的调用,x 是 , not (完美转发永远不会复制,如果是 by-value 参数,则会这样做)。仅仅是.在转发器中计算结果为左的原因是,命名右值引用成为左值表达式。const int iAconst intdeduced(1)int&&intxTintx
6赞 David G 4/1/2013
使用或这里有什么区别吗?还是只是语义上的差异?forwardmove
45赞 GManNickG 4/1/2013
@David:应该在没有显式模板参数的情况下调用,并且始终导致右值,而可能最终为右值。当您知道不再需要该值并希望将其移动到其他位置时使用,请使用 to 根据传递给函数模板的值执行此操作。std::movestd::forwardstd::movestd::forward
8赞 ShreevatsaR 1/31/2015
感谢您首先从具体的例子开始并激发问题;很有帮助!
50赞 sellibitze 8/30/2010 #3

在完全转发中,std::forward 用于将命名右值引用 t1 和 t2 转换为未命名右值引用。这样做的目的是什么?如果我们将 t1 和 t2 保留为 lvalue,这将如何影响被调用的函数内部?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

如果在表达式中使用命名右值引用,则它实际上是一个左值(因为您按名称引用对象)。请看以下示例:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

现在,如果我们这样称呼outer

outer(17,29);

我们希望将 17 和 29 转发到 #2,因为 17 和 29 是整数文字,因此是 rvalues。但是,由于表达式中的 和 是左值,因此您将调用 #1 而不是 #2。这就是为什么我们需要用 将引用转换回未命名的引用。因此,in 始终是左值表达式,而可能是右值表达式,具体取决于 。如果后者是左值引用,则后者只是一个左值表达式。并且仅在 outer 的第一个参数是左值表达式的情况下被推断为左值引用。t1t2inner(t1,t2);std::forwardt1outerforward<T1>(t1)T1T1T1

评论

3赞 NicoBerrogorry 6/28/2018
这是一种淡化的解释,但是一个做得很好和实用的解释。人们应该先阅读这个答案,然后如果需要,再深入研究
0赞 John 6/14/2020
@sellibitze 还有一个问题,在推导出 int a 时哪个陈述是正确的;f(a):“既然 a 是一个左值,那么 int(T&&) 等同于 int(int& &&)”或“使 T&& 等同于 int&,所以 T 应该是 int&”?我更喜欢后者。
109赞 user7610 5/1/2014 #4

我认为有一个实现 std::forward 的概念代码可以帮助理解。这是Scott Meyers演讲的幻灯片 An Effective C++11/14 Sampler

conceptual code implementing std::forward

代码中的函数是 。在前面的演讲中有一个(工作)实现。我在 libstdc++ 的 move.h 文件中找到了 std::forward 的实际实现,但它根本没有指导意义。movestd::move

从用户的角度来看,它的含义是有条件地转换为右值。如果我正在编写一个函数,该函数需要参数中的左值或右值,并且仅当它作为右值传入时才想将其作为右值传递给另一个函数,那么它可能会很有用。如果我没有将参数包装在 std::forward 中,它将始终作为普通引用传递。std::forward

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

果然,它打印出来了

std::string& version
std::string&& version

该代码基于前面提到的演讲中的一个示例。幻灯片 10,大约在 15:00 开始。

评论

3赞 Pharap 5/4/2018
你的第二个链接最终指向了一个完全不同的地方。
3赞 flamingo 11/16/2020
哇,很好的解释。我从这个视频开始:youtube.com/watch?v=srdwFMZY3Hg,但看完你的回答,我终于感觉到了。:)
7赞 Bill Chapman 2/23/2016 #5

没有明确说明的一点是处理得当。
程序:
static_cast<T&&>const T&

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

生产:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

请注意,“f”必须是模板函数。如果它只是定义为'void f(int&& a)',这是行不通的。

评论

0赞 barney 8/16/2016
好点子,所以静态投射中的T&&也遵循参考折叠规则,对吧?
5赞 colin 12/8/2017 #6

可能值得强调的是,转发必须与具有转发/通用引用的外部方法一起使用。允许单独使用forward作为以下语句,但除了引起混淆之外没有任何好处。标准委员会可能希望禁用这种灵活性,否则我们为什么不直接使用static_cast呢?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

在我看来,前进和前进是设计模式,是引入 r 值参考类型后的自然结果。除非禁止不正确的使用,否则我们不应该假设它被正确使用,否则不应该命名方法。

评论

0赞 SirGuy 12/15/2017
我不认为 C++ 委员会认为他们有责任“正确”使用语言习语,甚至不定义什么是“正确”用法(尽管他们当然可以给出指导)。为此,虽然一个人的老师、老板和朋友可能有责任以某种方式引导他们,但我相信 C++ 委员会(以及标准)没有这个责任。
0赞 colin 12/20/2017
是的,我刚刚阅读了 N2951,我同意标准委员会没有义务对函数的使用添加不必要的限制。但是这两个函数模板(move 和 forward)的名称确实有点令人困惑,只在库文件或标准文档(23.2.5 Forward/move helpers)中看到它们的定义。标准中的示例肯定有助于理解这个概念,但添加更多注释以使事情更清晰可能会有所帮助。
1赞 Sorush 10/28/2021 #7

从另一个角度来看,在通用引用赋值中处理右值时,可能需要保留变量的类型。例如

auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

使用 ,我们确保与 完全相同的类型。std::forwardzx

此外,不影响左值引用:std::forward

int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

仍然具有与 相同的类型。zx

所以,回到你的情况,如果内部函数有两个重载 和 ,你想传递赋值等变量,而不是一个。int&int&&zy

示例中的类型可以通过以下方式进行评估:

std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;

评论

2赞 HolyBlackCat 10/28/2021
std::forward<decltype(x)>(x)可以缩短为(假设是参考)。decltype(x)(x)x
0赞 Sorush 10/28/2021
@HolyBlackCat,好点子。我保留只是为了讨论。std::forward
-1赞 BlowMyMind 12/13/2023 #8

std::forward保留变量的原始 L/R 值类型

#include <iostream>
using namespace std;

void function2(int& num){ cout << "lvalue: " << num << endl;}
void function2(int&& num){ cout << "rvalue: " << num << endl;}

template <typename T>
void function1(T&& a) {
    function2(50);            // <- we pass "50" here, which is always a rvalue
    
    int b = 50;
    function2(b);             // <- we pass "b" here, which is always a lvalue

    function2(a);             // <- we pass "a" here, but is it a lvalue or rvalue?
                              //    with this syntax, even when "a" is passed in as a rvalue,
                              //    "function1" names it "a", it will always be a lvalue

    function2(forward<T>(a)); // <- we use "forward" to preserve its original l/r value type
    cout << endl;
}

int main() {
    function1(10);
    int c = 20;
    function1(c);
}

打印输出为:

rvalue: 50
lvalue: 50
lvalue: 10
rvalue: 10

rvalue: 50
lvalue: 50
lvalue: 20
lvalue: 20