如何向前工作?

可能的复制品:
使用 forward 的优点

我知道它是做什么的,什么时候使用它,但我 还是不能理解它是如何工作的。请尽可能详细,并解释当 std::forward将是不正确的,如果它是允许使用模板参数演绎。

我的困惑之一是: “如果它有一个名称,它就是一个左值”——如果是这样的话,为什么当我传递 thing&& xthing& x时,std::forward的行为会有所不同?

122891 次浏览

它工作是因为当调用完美转发时,类型 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 引用时,它实际上是一个左值。但是,forward通过上述方法检测到它实际上是一个 rvalue,并正确返回要转发的 rvalue。

首先,让我们看看 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是如何工作的。

我认为 std::forward作为 static_cast<T&&>的解释是令人困惑的。我们对强制转换的直觉是它将类型转换为其他类型——在这种情况下,它将转换为右值引用。不是的!所以我们用另一个神秘的东西来解释一个神秘的东西。Xeo 答案中的一张表格定义了这种特殊的演员阵容。但问题是,为什么?所以我的理解是:

假设我想传递给您一个 std::vector<T> v,您应该将它作为数据成员 _v存储在数据结构中。天真(且安全)的解决方案是始终将向量复制到其最终目的地。因此,如果您是通过一个中间函数(方法)来实现这一点,那么该函数应该声明为接受引用。(如果将其声明为按值取向量,那么将执行额外的完全不必要的副本。)

void set(const std::vector<T> & v) { _v = v; }

如果你手上有一个左值,这些都没问题,但是如果有一个右值呢?假设向量是调用函数 makeAndFillVector()的结果。如果你执行了一项直接的任务:

_v = makeAndFillVector();

编译器将 让开向量,而不是复制它。但是如果您引入一个中介 set(),那么关于参数的 rvalue 性质的信息就会丢失,并且会生成一个副本。

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()只定义了向量这一事实)。诀窍在于,当模板函数用左值实例化时,您希望该模板调用 set()的第一个版本,而当第二个模板函数用右值初始化时,您希望该模板调用 set()的第一个版本。

首先,这个函数的签名应该是什么? 答案是:

template<class T>
void perfectSet(T && t);

取决于您如何调用这个模板函数,T类型将以不同的方式神奇地推导出来。如果你用 lvalue 调用它:

std::vector<T> v;
perfectSet(v);

向量 v将通过引用传递,但是如果使用 rvalue 调用它:

perfectSet(makeAndFillVector());

(匿名)向量将通过 rvalue 引用传递。因此,C + + 11的魔法是有目的地建立在这样一种方式,即尽可能保留参数的 rvalue 性质。

现在,在 perfect tSet 中,您想要完美地将参数传递给 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的内容,这意味着原始内容变为空。

这个答案比我最初假设的时间要长得多; -)