在完美转发中,std::forward
用于将命名的右值引用t1
和t2
转换为未命名的右值引用。这样做的目的是什么?如果我们离开inner
&amp ;,那将如何影响被调用函数t1
t2
作为左值?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
答案 0 :(得分:719)
您必须了解转发问题。你可以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);
,因为它们不能绑定到左值引用。
下一次尝试可能是:
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!
最终解决方案正确处理所有事情......代价是无法维护。您提供f
的重载,以及所有 const和非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中,我们有机会解决这个问题。 One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code.所以我们必须找到另一种方式。
解决方案是使用新添加的 rvalue-references ;我们可以在推导rvalue-reference类型并创建任何所需结果时引入新规则。毕竟,我们现在不可能破坏代码。
如果给出对引用的引用(注释引用是包含T&
和T&&
的包含术语,则我们使用以下规则来计算结果类型:
“[given]类型TR是对类型T的引用,尝试创建类型”对cv TR的左值引用“创建类型”左值引用T“,同时尝试创建类型” rvue对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.否则,我们正常推导。这给出了所谓的通用引用(术语forwarding reference现在是官方引用)。
为什么这有用?因为组合我们保持跟踪类型的值类别的能力:如果它是左值,我们有一个左值引用参数,否则我们有一个rvalue-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需要获得与我们相同的价值类别!解决方案是:
static_cast<T&&>(x);
这是做什么的?考虑一下我们在deduce
函数中,我们已经传递了一个左值。这意味着T
是A&
,因此静态广播的目标类型为A& &&
,或仅A&
。由于x
已经是A&
,我们什么都不做,并留下左值参考。
当我们传递右值时,T
为A
,因此静态广播的目标类型为A&&
。强制转换导致rvalue表达式,它不能再传递给左值引用。我们维护了参数的值类别。
将这些放在一起可以让我们“完美转发”:
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);
答案 1 :(得分:49)
我认为有一个实现std :: forward的概念代码可以添加到讨论中。这是来自Scott Meyers talk An Effective C++11/14 Sampler
的幻灯片
代码中的函数move
为std::move
。在那次谈话中,它有一个(工作)实现。我在文件move.h中找到了actual implementation of std::forward in libstdc++,但它根本没有用处。
从用户的角度来看,它的含义是std::forward
是对rvalue的条件转换。如果我正在编写一个函数,该函数需要参数中的左值或右值,并且只有当它作为右值传入时才想将它作为右值传递给另一个函数。如果我没有将参数包装在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
该代码基于前面提到的谈话中的示例。从开始大约15:00滑10,
答案 2 :(得分:23)
在完美转发中,std :: forward用于将命名的右值引用t1和t2转换为未命名的右值引用。这样做的目的是什么?如果我们离开t1&amp; t2是左值?
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(std::forward<T1>(t1), std::forward<T2>(t2)); }
如果在表达式中使用命名的右值引用,它实际上是一个左值(因为您按名称引用了对象)。请考虑以下示例:
void inner(int &, int &); // #1
void inner(int &&, int &&); // #2
现在,如果我们像这样打电话给outer
outer(17,29);
我们希望将17和29转发到#2,因为17和29是整数文字和rvalues。但由于表达式t1
中的t2
和inner(t1,t2);
是左值,因此您将调用#1而不是#2。这就是为什么我们需要将引用转回到std::forward
的未命名引用。因此,t1
中的outer
始终是左值表达式,而forward<T1>(t1)
可能是右值表达式,具体取决于T1
。如果T1
是左值引用,则后者仅是左值表达式。如果外部的第一个参数是左值表达式,则T1
仅被推导为左值引用。
答案 3 :(得分:11)
如果我们离开t1&amp; t2是左值?
如果在实例化后,T1
属于char
类型且T2
属于某个类,则您希望每个副本传递t1
并t2
每const
个引用。好吧,除非inner()
根据非const
引用获取它们,也就是说,在这种情况下,您也希望这样做。
尝试编写一组outer()
函数,这些函数在没有右值引用的情况下实现它,推导出从inner()
类型传递参数的正确方法。我认为你需要2 ^ 2个东西,相当大的模板元素来推断论证,并且有很多时间来适应所有情况。
然后有人带着inner()
来获取每个指针的参数。我认为现在是3 ^ 2。 (或者4 ^ 2.太好了,我不能试图去思考const
指针是否会产生影响。)
然后想象你想要为五个参数做这个。或七。
现在你知道为什么一些聪明的头脑想出了“完美的转发”:它让编译器为你做这一切。
答案 4 :(得分:3)
尚未明确表达的观点是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&amp;&amp; a)',那么这不起作用。
答案 5 :(得分:1)
值得强调的是,必须与具有转发/通用引用的外部方法一起使用forward。允许使用forward作为以下语句,但除了引起混淆之外没有任何好处。标准委员会可能希望禁用这种灵活性,否则我们为什么不使用static_cast?
std::forward<int>(1);
std::forward<std::string>("Hello");
在我看来,移动和前进是设计模式,这是引入r值引用类型后的自然结果。除非禁止使用不正确的用法,否则我们不应该假定它被正确使用的方法命名。