523

在完美转发中,std::forward用于将命名的右值引用转换为t1未命名的t2右值引用。这样做的目的是什么?inner如果我们将t1&作为左值,这将如何影响被调用的函数t2

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

7 回答 7

938

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

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


最简单的是使用左值引用:

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

但这无法处理临时值: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);
}

这解决了上述问题,但触发器。它现在不允许E有非常量参数:

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

第三次尝试接受 const-references,但随后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);

这是做什么的?考虑我们在deduce函数内部,并且我们已经被传递了一个左值。这意味着T是 a A&,因此静态转换的目标类型是A& &&,或者只是A&。由于x已经是一个A&,我们什么也不做,只剩下一个左值引用。

当我们被传递一个右值时,Tis A,所以静态转换的目标类型是A&&。强制转换产生一个右值表达式,它不能再被传递给一个左值引用。我们维护了参数的值类别。

把这些放在一起给我们“完美的转发”:

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

f接收到一个左值时,E获取一个左值。当f接收到一个右值时,E获取一个右值。完美的。


当然,我们想摆脱丑陋的东西。static_cast<T&&>是神秘而奇怪的记忆;让我们创建一个名为 的实用函数forward,它做同样的事情:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
于 2010-08-27T07:59:47.757 回答
89

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

实现 std::forward 的概念代码

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

从用户的角度来看,它的含义是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 开始。

于 2014-05-01T15:44:45.977 回答
44

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

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 是整数文字,因此是右值。但由于t1t2在表达式inner(t1,t2);中是左值,你会调用#1 而不是#2。这就是为什么我们需要将引用转换回未命名的引用std::forward。因此,t1inouter始终是左值表达式,而forward<T1>(t1)可能是右值表达式,具体取决于T1. T1如果是左值引用,后者只是一个左值表达式。并且T1仅在 outer 的第一个参数是左值表达式的情况下被推断为左值引用。

于 2010-08-29T20:34:06.257 回答
12

如果我们将 t1 和 t2 保留为左值,这将如何影响被调用的函数内部?

如果在实例化之后T1是 typechar并且T2是一个类,你想要传递t1每个副本和t2每个const引用。好吧,除非inner()根据非const引用来获取它们,也就是说,在这种情况下,您也想这样做。

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

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

然后想象你想对五个参数执行此操作。或七。

现在您知道为什么一些聪明人会想出“完美转发”:它让编译器为您完成所有这些工作。

于 2010-08-27T07:44:43.293 回答
6

尚未明确的一点是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)' 这是行不通的。

于 2016-02-23T15:37:43.243 回答
4

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

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

在我看来,move 和 forward 是设计模式,是引入 r 值引用类型后的自然结果。我们不应该假设一个方法被正确使用,除非不正确的使用是被禁止的。

于 2017-12-07T20:20:09.703 回答
1

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

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

使用std::forward,我们确保z与 具有完全相同的类型x

此外,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&

仍然z具有与 相同的类型x

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

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

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

于 2021-10-27T21:29:42.527 回答