【问题标题】:How does std::forward work? [duplicate]std::forward 是如何工作的? [复制]
【发布时间】:2012-01-21 12:47:38
【问题描述】:

可能重复:
Advantages of using forward

我知道它的作用以及何时使用它,但我仍然无法理解它是如何工作的。如果允许使用模板参数推导,请尽可能详细并说明std::forward何时会不正确。

我的部分困惑是: “如果它有一个名字,它就是一个左值”——如果是这样,当我通过 thing&& xthing& x 时,为什么 std::forward 的行为会有所不同?

【问题讨论】:

  • Answered here. std::forward 实际上只是 static_cast<T&&> 的语法糖。
  • 为什么不能让类型被推断的简短答案:在template <typename T> void foo(T && x); 的主体中,x 的类型与T 推断的类型不同。

标签: c++ c++11


【解决方案1】:

我认为将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() 仅针对向量定义)。诀窍是您希望此模板在使用左值实例化模板函数时调用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,编译器将保留它的右值性,并且将调用set() 的右值引用版本。这个版本移动了t的内容,也就是说原来的变成了空的。

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

【讨论】:

  • void set(**const** std::vector &amp; v) { _v = v; } 不要把事情弄得过于复杂。
  • "在这种情况下,它将转换为右值引用。不是!" - 是的!在您的perfectSet 内部,t 已经 一个左值。使用static_cast(或std::forward),我们将其改回右值。
  • @Xeo:除非您调用完美集并引用向量。如:向量 v;矢量 & vr;完美集(虚拟现实);当您将左值引用转换为右值引用时,结果仍然是左值引用。我就是这个意思。
  • @Bartosz:即便如此,您也没有强制转换为右值引用。正如我在回答中所说,您只需转换为左值引用,即无操作。参考折叠规则对此进行了排序。
  • 如何在 cmets 中格式化代码?缩进技巧不起作用。
【解决方案2】:

首先我们来看看std::forward按照标准做了什么:

§20.2.3 [forward] p2

返回: static_cast&lt;T&amp;&amp;&gt;(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 的工作原理。

【讨论】:

  • “(无耻地从这个答案中窃取。)”不要再想了。他们从这里偷了它:open-std.org/jtc1/sc22/wg21/docs/papers/2002/… :-)
  • 我真正的困惑是为什么 std::forward 不允许使用模板参数推导,但我不想用这些话问它,因为我之前已经尝试过一次但没有我能理解的结果.我想我现在已经想通了 (stackoverflow.com/a/8862379/369872)
  • 我认为最后一个代码 sn-p 不起作用,因为您使用了一个原始类型 int ,它没有移动构造函数。您应该使用类似 std::vector 或具有移动构造函数的字符串。
  • 我想我永远不会知道,但是……为什么要投反对票?我错过了什么?我说错了吗?
  • 我会给你一篮鲜花和一大块巧克力。谢谢!
【解决方案3】:

之所以有效,是因为在调用完美转发时,类型 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通过上面的方法检测到它实际上是一个右值,并正确返回一个右值进行转发。

【讨论】:

  • 啊哈!您能否为 std::string &s、std::string&& s、const std::string&& s、std::string* s、std::string* const s 添加更多示例(以及 T 是什么)?
  • @Dave:不,不是真的。有很多教程可以更彻底地参考折叠。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-07
相关资源
最近更新 更多