使用forward的优点
在完美转发中,使用std::forward
将指定的右值引用t1
和t2
转换为未命名的右值引用。 这样做的目的是什么? 如果我们将t1
和t2
作为左值,那么这将如何影响被调用的函数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_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!
最终的解决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
函数中,而且我们已经通过了一个左值。 这意味着T
是A&
,所以静态types的目标types是A& &&
,或者只是A&
。 由于x
已经是A&
,我们什么都不做,只剩下一个左值引用。
当我们传递一个右值时, T
是A
,所以静态转换的目标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::move
是std::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。 但是由于t1
和t2
在expression式inner(t1,t2);
是左值,你会调用#1而不是#2。 这就是为什么我们需要将引用变回到未命名的引用与std::forward
。 所以, outer
t1
始终是一个左值expression式,而forward<T1>(t1)
可能是一个取决于T1
的右值expression式。 如果T1
是一个左值引用,后者只是一个左值expression式。 而且只有当第一个参数是一个左值expression式时, T1
才被推断为左值引用。
如果我们将t1和t2作为左值,那么这将如何影响被调用函数inner?
如果在实例化之后, T1
是char
types的,而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)',这是行不通的。