c++ - 使用 std::forward 的主要目的是什么以及它解决了哪些问题?

标签 c++ c++11 rvalue-reference c++-faq perfect-forwarding

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

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

最佳答案

您必须了解转发问题。您可以read the entire problem in detail ,但我会总结一下。

基本上,给定表达式 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); ,因为它们不能绑定(bind)到左值引用。

下一次尝试可能是:

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

这解决了上述问题,因为“ const X& binds to everything”,包括左值和右值,但这会导致一个新问题。它现在不允许 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_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!

最终的解决方案可以正确处理所有事情……但代价是无法维护。您提供 f 的重载, all 是 const 和 non-const 的组合:

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 中,我们有机会解决这个问题。 One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code.所以我们必须另谋出路。

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

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

"[given] a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type 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 的左值引用。否则,我们正常推导。这提供了所谓的通用引用(forwarding reference 现在是官方术语)。

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

在代码中:

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 需要获得与我们获得的相同类型的值(value)类别!解决办法是这样的:

static_cast<T&&>(x);

这是做什么的?假设我们在 deduce 内函数,我们已经被传递了一个左值。这意味着 TA& ,因此静态转换的目标类型是 A& && , 或只是 A& .从 x已经是 A& ,我们什么都不做,只剩下一个左值引用。

当我们被传递一个右值时,TA ,所以静态转换的目标类型是 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);

关于c++ - 使用 std::forward 的主要目的是什么以及它解决了哪些问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3582001/

相关文章:

c++ - std::make_shared/std::make_unique 不使用列表初始化有什么原因吗?

c++ - Const 右值编译器区别

c++ - C/C++ 用十六进制初始化 double

c++ - 防止 std::ifstream 创建空文件

c++ - 如何在 QAbstractItemModel 中设置 QCheckBox?

c++ - 在 C++11 中实现递归代理模式

c++ - 即使 XZY 具有非复制约束,构造助手 make_XYZ 也允许 RVO 和类型推导

c++ - 在 C++17 中,为什么类模板与函数模板的指针类型推导明显不一致?

c++ - 以人类可读的形式在 QSettings 中保存自定义 QMap 模板实例化

c++ - 未在此范围内声明(C++中的数组)