使用forward的优点

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

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

基本上,给定expression式E(a, b, ... , c) ,我们希望expression式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有非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! 

最终的解决scheme能够正确处理所有事情,而且不可能维护。 你提供了f重载,包括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个参数需要2 N个组合,这是一场噩梦。 我们想自动做这个。

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


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

解决方法是使用新添加的右值引用 ; 我们可以在推导右值引用types时引入新的规则并创build任何期望的结果。 毕竟,我们现在不可能破坏代码。

如果给定了一个参考的参考(注意参考是一个涵盖T&T&&术语),我们使用以下规则来确定结果types:

“[给定]typesTR是对Ttypes的引用,尝试创buildtypes”cv TR的左值引用“会创buildtypes”左值对T的引用“,而尝试创buildtypes”右值引用“ cv TR“创buildtypesTR”。

或者以表格forms:

 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.否则,我们推导正常。 这就提供了所谓的通用参考 (术语转发参考现在是正式参考 )。

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

在代码中:

 template <typename T> void deduce(T&& x); int i; deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&) deduce(1); // deduce<int>(int&&) 

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

 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函数中,而且我们已经通过了一个左值。 这意味着TA& ,所以静态types的目标types是A& && ,或者只是A& 。 由于x已经是A& ,我们什么都不做,只剩下一个左值引用。

当我们传递一个右值时, TA ,所以静态转换的目标types是A&& 。 转换会产生一个右值expression式, 不能再传递给左值引用 。 我们维护了参数的值类别。

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

 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讲的一个有效的C ++ 11/14采样器

实现std :: forward的概念性代码

函数在代码中std::movestd::move 。 在那次演讲中,有一个(工作)实现。 我在libstdc ++文件move.h中发现了std :: forward的实际实现 ,但这并没有什么启发意义。

从用户的angular度来看,它的含义是, 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点左右。

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

如果你在一个expression式中使用了一个名为rvalue的引用,那么它实际上是一个左值(因为你是通过名字来引用这个对象的)。 考虑下面的例子:

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

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

 outer(17,29); 

我们希望17和29被转发到#2,因为17和29是整数文字和rvalues。 但是由于t1t2在expression式inner(t1,t2); 是左值,你会调用#1而不是#2。 这就是为什么我们需要将引用变回到未命名的引用与std::forward 。 所以, outer t1始终是一个左值expression式,而forward<T1>(t1)可能是一个取决于T1的右值expression式。 如果T1是一个左值引用,后者只是一个左值expression式。 而且只有当第一个参数是一个左值expression式时, T1才被推断为左值引用。

如果我们将t1和t2作为左值,那么这将如何影响被调用函数inner?

如果在实例化之后, T1chartypes的,而T2是一个类的,那么您希望每个副本传递t1 ,每个const引用传递t2 。 那么,除非inner()把它们作为非const引用,否则也是你想这样做的。

尝试编写一套不带右值引用的outer()函数,推导出从inner()的types传递参数的正确方法。 我想你会需要2 ^ 2的东西,相当大的模板元的东西来推断的论点,并有很多时间来得到这个正确的所有情况下。

然后有人来一个inner() ,每个指针参数。 我认为现在做3 ^ 2。 (或者4 ^ 2,我不敢想象const指针是否会const )。

然后想象你想为五个参数做这个。 还是七

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

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