在完全转发中,std::forward用于将命名右值引用t1和t2转换为未命名的右值引用。这样做的目的是什么?如果我们留下t1 &t2作为左值?
std::forward
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引用使用它们,也就是说,在这种情况下,您也想这样做。
T1
char
T2
const
inner()
尝试编写一组outer()函数,在不使用右值引用的情况下实现此功能,推导从inner()的类型传递参数的正确方法。我认为你需要2^2的东西,非常繁重的模板元的东西来推导参数,而且要花很多时间来正确地处理所有情况。
outer()
然后有人提出了inner(),每个指针接受参数。我想现在是3^2。(或4 ^ 2。见鬼,我懒得去想const指针是否会有所不同。)
然后想象一下你想用五个参数来做这个。或7。
现在你知道为什么有些聪明的人想到了“完美转发”:它让编译器为你做所有这些。
你必须理解转发问题。你可以详细阅读整道题,但我要总结一下。
基本上,给定表达式E(a, b, ... , c),我们希望表达式f(a, b, ... , c)是等价的。在c++ 03中,这是不可能的。有很多尝试,但都在某些方面失败了。
E(a, b, ... , c)
f(a, b, ... , c)
最简单的方法是使用左值引用:
template <typename A, typename B, typename C> void f(A& a, B& b, C& c) { E(a, b, c); }
但是这不能处理临时值(右值):f(1, 2, 3);,因为这些值不能绑定到左值引用。
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参数:
const X&
E
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:
const_cast
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的重载:
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个参数需要2个N组合,这是一场噩梦。我们希望自动完成。
(这实际上是我们在c++ 11中让编译器为我们做的事情。)
在c++ 11中,我们有机会修复这个问题。所以我们必须找到另一种方法。
解决方案是使用新添加的rvalue-references;我们可以在推导右值引用类型时引入新的规则,并创建任何想要的结果。毕竟,我们现在不可能破解代码。
如果给定一个引用到一个引用(注意reference是一个包含了T&和T&&的术语),我们使用以下规则来计算结果类型:
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的左值引用。否则,正常演绎。这就给出了所谓的普遍的引用(术语< em >转发参考< / em >现在是官方名称)。
为什么这个有用?因为我们保持了跟踪类型的值类别的能力:如果它是左值,我们有一个左值引用形参,否则我们有一个右值引用形参。
在代码:
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);
这有什么用?假设我们在deduce函数中,并且传递了一个左值。这意味着T是A&,因此静态强制转换的目标类型是A& &&,或者只是A&。由于x已经是A&,我们什么都不做,只留下一个左值引用。
deduce
T
A&
A& &&
x
当传递一个右值时,T是A,因此静态强制转换的目标类型是A&&。强制转换的结果是一个右值表达式不能再传递给左值引用的。我们维护了参数的值类别。
A
A&&
把这些放在一起,我们就得到了“完美的转发”:
template <typename A> void f(A&& a) { E(static_cast<A&&>(a)); }
当f接收到一个左值时,E接收到一个左值。当f接收到右值时,E获得右值。完美的。
当然,我们要去掉丑的部分。static_cast<T&&>是一个神秘而奇怪的记忆;让我们创建一个名为forward的实用函数,它做同样的事情:
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) { inner(std::forward<T1>(t1), std::forward<T2>(t2)); }
在完全转发中,std::forward用于将命名右值引用t1和t2转换为未命名右值引用。这样做的目的是什么?如果我们保留t1 &,会对被调用的函数产生怎样的影响;T2是左值?
如果在表达式中使用了命名的右值引用,它实际上是一个左值(因为您通过名称引用对象)。考虑下面的例子:
void inner(int &, int &); // #1 void inner(int &&, int &&); // #2
现在,如果我们这样调用outer
outer
outer(17,29);
我们希望将17和29转发到#2,因为17和29是整数字面量和右值。但是由于表达式inner(t1,t2);中的t1和t2是左值,您将调用#1而不是#2。这就是为什么我们需要使用std::forward将引用转换回未命名的引用。因此,outer中的t1始终是一个左值表达式,而forward<T1>(t1)可能是一个右值表达式,这取决于T1。如果T1是一个左值引用,则后者只是一个左值表达式。而T1只有在outer的第一个参数是左值表达式的情况下才会被推导为左值引用。
inner(t1,t2);
forward<T1>(t1)
我认为有一个概念性代码实现std::forward可以帮助理解。这是Scott Meyers演讲一个有效的c++ 11/14采样器的一张幻灯片
代码中的move函数为std::move。在前面的讨论中有一个(正在工作的)实现。我发现std::forward在libstdc++中的实际实现,在文件move.h中,但它一点也不具有指导意义。
move
std::move
从用户的角度来看,它的含义是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点开始。
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具有完全相同的类型。
z
此外,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赋值。
int&
int&&
y
示例中的类型可以通过以下方式进行评估:
std::cout<<is_same_v<int&,decltype(z)>; std::cout<<is_same_v<int&&,decltype(z)>;