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

当前回答

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

其他回答

如果我们将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可以帮助理解。这张幻灯片来自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点开始。

值得强调的是,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)>;