在完全转发中,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));
}

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

如果在实例化之后,T1是char类型,T2是一个类,那么您希望每个复制传递T1,每个const引用传递T2。好吧,除非inner()对每个非const引用都接受它们,也就是说,在这种情况下,您也想这样做。

尝试编写一组outer()函数,在不使用右值引用的情况下实现此功能,推导出从inner()类型传递参数的正确方法。我认为你需要2^2的东西,非常繁重的模板元的东西来推导参数,而且要花很多时间来正确地处理所有情况。

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

然后想象一下你想用五个参数来做这个。或7。

现在你知道为什么有些聪明的人想到了“完美转发”:它让编译器为你做所有这些。


你必须理解转发问题。你们可以详细阅读整道题,但我只做个总结。

基本上,给定表达式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);
}

但是这不能处理临时值(rvalues): 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&绑定到所有东西”,包括左值和右值,但这导致了一个新问题。它现在不允许E拥有非const参数:

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

第三次尝试接受const引用,但随后const_cast将const去掉:

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个参数需要2N个组合,简直是噩梦。我们希望自动完成。

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


在c++ 11中,我们有机会修复这个问题。一种解决方案修改现有类型的模板演绎规则,但这可能会破坏大量代码。所以我们得另想办法。

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

如果给定一个引用到另一个引用(注意reference是一个包含了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);

这有什么用?假设我们在演绎函数内部,我们被传递了一个左值。这意味着T是a &,因此静态强制转换的目标类型是a &&,或者只是a &。由于x已经是一个A&,我们什么都不做,只剩下一个左值引用。

当我们被传递一个右值时,T是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);

在完全转发中,std::forward用于将命名右值引用t1和t2转换为未命名右值引用。这样做的目的是什么?如果我们将t1和t2保留为左值,会对被调用函数内部产生什么影响? template <typename T1, typename T2> void outer(T1&& T1, T2&& T2) { 内部(std:: < T1 > (T1)、std::转发< T2 > (T2)); }

如果在表达式中使用了命名的右值引用,它实际上是一个左值(因为您通过名称引用对象)。考虑下面的例子:

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

现在,如果我们像这样调用外层

outer(17,29);

we would like 17 and 29 to be forwarded to #2 because 17 and 29 are integer literals and as such rvalues. But since t1 and t2 in the expression inner(t1,t2); are lvalues, you'd be invoking #1 instead of #2. That's why we need to turn the references back into unnamed references with std::forward. So, t1 in outer is always an lvalue expression while forward<T1>(t1) may be an rvalue expression depending on T1. The latter is only an lvalue expression if T1 is an lvalue reference. And T1 is only deduced to be an lvalue reference in case the first argument to outer was an lvalue expression.


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

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

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


还有一点没有说清楚,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)这是行不通的。


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

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

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


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

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&和int&&有两个重载,你想传递的变量是z赋值,而不是y赋值。

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

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