我知道它的作用以及何时使用它但我仍然无法理解其工作原理。请尽可能详细,并解释std::forward
如果允许使用模板参数扣除则不正确。
我的一部分困惑是这样的:
“如果它有一个名字,那就是一个左值” - 如果是这种情况,那么当我通过std::forward
vs thing&& x
时,为什么thing& x
表现不同?
答案 0 :(得分:145)
我认为std::forward
对static_cast<T&&>
的解释令人困惑。我们对强制转换的直觉是它将类型转换为其他类型 - 在这种情况下,它将转换为右值引用。不是!所以我们用另一个神秘的东西解释一个神秘的东西。这个特殊的演员表由Xeo的答案中的表格定义。但问题是:为什么?所以这是我的理解:
假设我想将您应该存储在数据结构中的std::vector<T> v
作为数据成员_v
传递给您。天真(和安全)的解决方案是始终将矢量复制到其最终目的地。因此,如果您通过中间函数(方法)执行此操作,则应将该函数声明为引用。 (如果您将其声明为按值获取向量,则您将执行额外的完全不必要的副本。)
void set(std::vector<T> & v) { _v = v; }
如果你手上有一个左值,这一切都很好,但左手呢?假设向量是调用函数makeAndFillVector()
的结果。如果您执行了直接分配:
_v = makeAndFillVector();
编译器移动向量而不是复制它。但是如果你引入一个中间人set()
,那么关于你论证的右值性质的信息就会丢失,并且会复制一份。
set(makeAndFillVector()); // set will still make a copy
为了避免此副本,您需要“完美转发”,这将导致每次都有最佳代码。如果给你一个左值,你希望你的函数作为左值并进行复制。如果给你一个右值,你希望你的函数将它作为右值处理并移动它。
通常你会通过分别为左值和右值重载函数set()
来做到这一点:
set(std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }
但现在想象一下,您正在编写一个接受T
的模板函数,并使用set()
调用T
(不要担心我们set()
的事实仅为向量定义。诀窍在于,当模板函数使用左值实例化时,您希望此模板调用set()
的第一个版本,而当使用右值初始化模板函数时,您希望调用第二个版本。
首先,这个功能的签名应该是什么?答案是:
template<class T>
void perfectSet(T && t);
根据您调用此模板函数的方式,类型T
将在某种程度上神奇地推断出来。如果你用左值调用它:
std::vector<T> v;
perfectSet(v);
向量v
将通过引用传递。但如果你用右值调用它:
perfectSet(makeAndFillVector());
(匿名)向量将通过右值引用传递。所以C ++ 11魔术是有目的地设置的,以便在可能的情况下保留参数的右值特性。
现在,在perfectSet中,您希望将参数完美地传递给set()
的正确重载。这是std::forward
所必需的地方:
template<class T>
void perfectSet(T && t) {
set(std::forward<T>(t));
}
如果没有std :: forward,编译器必须假设我们想通过引用传递t。为了说服自己这是真的,请比较以下代码:
void perfectSet(T && t) {
set(t);
set(t); // t still unchanged
}
到此:
void perfectSet(T && t) {
set(std::forward<T>(t));
set(t); // t is now empty
}
如果您没有明确转发t
,编译器必须防御性地假设您可能再次访问t并选择了set的左值引用版本。但是如果你转发t
,编译器将保留它的rvalue-ness,并且将调用set()
的rvalue引用版本。此版本移动t
的内容,这意味着原始内容变为空。
这个答案比我最初假设的要长得多; - )
答案 1 :(得分:131)
首先,让我们看一下std::forward
根据标准做的事情:
§20.2.3 [forward] p2
返回:
static_cast<T&&>(t)
(其中T
是显式指定的模板参数,t
是传递的参数。)
现在请记住参考折叠规则:
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)
(从this answer无耻地偷走。)
然后让我们来看一个想要采用完美转发的课程:
template<class T>
struct some_struct{
T _v;
template<class U>
some_struct(U&& v)
: _v(static_cast<U&&>(v)) {} // perfect forwarding here
// std::forward is just syntactic sugar for this
};
现在是一个示例调用:
int main(){
some_struct<int> s1(5);
// in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
// ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
// with rvalue reference 'v' bound to rvalue '5'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
// this just turns 'v' back into an rvalue
// (named rvalue references, 'v' in this case, are lvalues)
// huzzah, we forwarded an rvalue to the constructor of '_v'!
// attention, real magic happens here
int i = 5;
some_struct<int> s2(i);
// in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
// applying the reference collapsing rules yields 'int&' (& + && -> &)
// ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
// with lvalue reference 'v' bound to lvalue 'i'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
// after collapsing rules: 'static_cast<int&>(v)'
// this is a no-op, 'v' is already 'int&'
// huzzah, we forwarded an lvalue to the constructor of '_v'!
}
我希望这个循序渐进的答案可以帮助您和其他人了解std::forward
的工作原理。
答案 2 :(得分:0)
它的工作原理是,当调用完美转发时,类型T 不值类型,它也可能是引用类型。
例如:
template<typename T> void f(T&&);
int main() {
std::string s;
f(s); // T is std::string&
const std::string s2;
f(s2); // T is a const std::string&
}
因此,forward
只需查看显式类型T即可查看真正传递的内容。当然,如果我记得的话,这样做的确切实现是非繁琐的,但这就是信息的所在。
当你引用一个名为rvalue reference 的时,那确实是一个左值。但是,forward
通过上述方法检测到它实际上是一个右值,并正确返回要转发的右值。