std::forward 如何工作
How does std::forward work?
可能的重复项:
使用前向的优点
我知道它的作用以及何时使用它,但我仍然无法理解它是如何工作的。请尽可能详细,并解释如果允许使用模板参数推导std::forward
何时不正确。
我的部分困惑是这样的:"如果它有一个名字,那就是一个左值"——如果是这样的话,为什么当我通过thing&& x
与thing& x
时,std::forward
的行为会有所不同?
我认为std::forward
解释为static_cast<T&&>
令人困惑。我们对强制转换的直觉是它将类型转换为其他类型 - 在这种情况下,它将转换为右值引用。不是!所以我们用另一个神秘的东西来解释一件神秘的事情。这个特殊的演员表由Xeo的答案中的表格定义。但问题是:为什么?所以我的理解是:
假设我想通过一个std::vector<T> v
,您应该将其作为数据成员存储在数据结构中_v
。天真(且安全(的解决方案是始终将矢量复制到其最终目的地。因此,如果您通过中介函数(方法(执行此操作,则应将该函数声明为接受引用。(如果您将其声明为按值获取向量,您将执行额外的完全不必要的复制。
void set(const std::vector<T> & v) { _v = v; }
如果你手里有一个左值,这一切都很好,但是右值呢?假设向量是调用函数 makeAndFillVector()
的结果。如果您执行了直接分配:
_v = makeAndFillVector();
编译器将移动向量而不是复制它。但是,如果你引入一个中介,set()
,关于你的论点的正确性质的信息将丢失,并且会复制。
set(makeAndFillVector()); // set will still make a copy
为了避免这种复制,你需要"完美转发",这将导致每次都产生最佳代码。如果给你一个左值,你希望你的函数把它当作左值并制作一个副本。如果给你一个右值,你希望你的函数把它当作右值并移动它。
通常,您将通过分别重载左值和右值的函数set()
来实现:
set(const std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }
但是现在假设您正在编写一个模板函数,该函数接受T
并使用该T
调用set()
(不要担心我们的set()
仅为向量定义的事实(。诀窍在于,您希望此模板在使用 lvalue 实例化模板函数时调用第一个版本的 set()
,在使用 rvalue 初始化时调用第二个版本。
首先,这个函数的签名应该是什么?答案是这样的:
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
,编译器将保留它的右值性,并将调用set()
的右值引用版本。此版本移动了t
的内容,这意味着原始版本变为空。
这个答案比我最初假设的要长得多;-(
首先,我们来看看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)
(从这个答案中无耻地窃取。
然后让我们看一个想要采用完美转发的类:
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
是如何工作的。
它之所以有效,是因为当调用完美转发时,类型 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 以查看您真正传递了它的内容。当然,如果我记得的话,这样做的确切实现是非竞争性的,但这就是信息所在。
当您引用命名的右值引用时,这确实是一个左值。但是,forward
通过上述方法检测到它实际上是一个右值,并正确返回要转发的右值。
- QSqlquery prepare()和bindvalue()不工作
- 导入库可以跨dll版本工作吗
- 以螺旋方式打印矩阵的程序.(工作不好)
- 对象指针在c++中是如何工作的
- 为什么在Windows上的VS 2019和Clang 9中"size_t"在没有标题的情况下工作
- VSOMEIP-2个设备之间的通信(TCP/UDP)不工作
- 为字符串中每 N 个字符插入空格的函数没有按照我认为的方式工作?
- C++为线程工作动态地分割例程
- 为什么我的 std::ref 无法按预期工作?
- 布尔比较运算符是如何在C++中工作的
- SampleConsensusPrerejective(ext.RANSAC)是如何真正工作的
- 不确定要在我的main中放入什么才能使我的代码正常工作
- 为什么std::condition_variable notify_all的工作速度比notify_one快(对于随机请
- <<操作员在下面的行中工作
- 有人能解释一下为什么下界是这样工作的吗C++的
- ExtractIconEx:可以工作,但偶尔会崩溃
- C++中的memset函数工作不正常
- 当我在第一个循环中使用"auto"时,它工作正常,但是使用"int"它会给出错误,为什么?
- 当 int 方法工作正常时,void 方法有何不同,或者为什么我不能调用 void 方法?
- sdl软件渲染器不工作,工作在硬件加速的一个