std::forward 如何工作,特别是在传递左值/右值引用时?

发布于 2024-12-21 19:42:43 字数 441 浏览 2 评论 0原文

可能的重复:
std::forward 的主要用途是什么以及它解决了哪些问题?

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

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

Possible Duplicate:
What are the main purposes of std::forward and which problems does it solve?

I know what it does and when to use it but I still can't wrap my head around how it works. Please be as detailed as possible and explain when std::forward would be incorrect if it was allowed to use template argument deduction.

Part of my confusion is this:
"If it has a name, it's an lvalue" - if that's the case why does std::forward behave differently when I pass thing&& x vs thing& x?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

夏九 2024-12-28 19:42:43

我认为将 std::forward 解释为 static_cast 令人困惑。我们对强制转换的直觉是,它将一种类型转换为其他类型——在本例中,它将转换为右值引用。它不是!所以我们正在用一种神秘的事物来解释另一种神秘的事物。这个特定的演员阵容是由 Xeo 的答案中的表格定义的。但问题是:为什么?所以这是我的理解:

假设我想向您传递一个 std::vector; 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 并调用 的模板函数>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 并选择左值引用版本的集合。但如果您转发t,编译器将保留它的右值性,并且将调用set() 的右值引用版本。这个版本移动了t的内容,也就是说原来的变成了空。

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

I think the explanation of std::forward as static_cast<T&&> is confusing. Our intuition for a cast is that it converts a type to some other type -- in this case it would be a conversion to an rvalue reference. It's not! So we are explaining one mysterious thing using another mysterious thing. This particular cast is defined by a table in Xeo's answer. But the question is: Why? So here's my understanding:

Suppose I want to pass you an std::vector<T> v that you're supposed to store in your data structure as data member _v. The naive (and safe) solution would be to always copy the vector into its final destination. So if you are doing this through an intermediary function (method), that function should be declared as taking a reference. (If you declare it as taking a vector by value, you'll be performing an additional totally unnecessary copy.)

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

This is all fine if you have an lvalue in your hand, but what about an rvalue? Suppose that the vector is the result of calling a function makeAndFillVector(). If you performed a direct assignment:

_v = makeAndFillVector();

the compiler would move the vector rather than copy it. But if you introduce an intermediary, set(), the information about the rvalue nature of your argument would be lost and a copy would be made.

set(makeAndFillVector()); // set will still make a copy

In order to avoid this copy, you need "perfect forwarding", which would result in optimal code every time. If you're given an lvalue, you want your function to treat it as an lvalue and make a copy. If you're given an rvalue, you want your function to treat it as an rvalue and move it.

Normally you would do it by overloading the function set() separately for lvalues and rvalues:

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

But now imagine that you're writing a template function that accepts T and calls set() with that T (don't worry about the fact that our set() is only defined for vectors). The trick is that you want this template to call the first version of set() when the template function is instantiated with an lvalue, and the second when it's initialized with an rvalue.

First of all, what should the signature of this function be? The answer is this:

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

Depending on how you call this template function, the type T will be somewhat magically deduced differently. If you call it with an lvalue:

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

the vector v will be passed by reference. But if you call it with an rvalue:

perfectSet(makeAndFillVector());

the (anonymous) vector will be passed by rvalue reference. So the C++11 magic is purposefully set up in such a way as to preserve the rvalue nature of arguments if possible.

Now, inside perfectSet, you want to perfectly pass the argument to the correct overload of set(). This is where std::forward is necessary:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>(t));
}

Without std::forward the compiler would have to assume that we want to pass t by reference. To convince yourself that this is true, compare this code:

void perfectSet(T && t) {
    set(t);
    set(t); // t still unchanged
}

to this:

void perfectSet(T && t) {
    set(std::forward<T>(t));
    set(t); // t is now empty
}

If you don't explicitly forward t, the compiler has to defensively assume that you might be accessing t again and chose the lvalue reference version of set. But if you forward t, the compiler will preserve the rvalue-ness of it and the rvalue reference version of set() will be called. This version moves the contents of t, which means that the original becomes empty.

This answer turned out much longer than what I initially assumed ;-)

‖放下 2024-12-28 19:42:43

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

§20.2.3 [forward] p2

返回: static_cast(t)

(其中 T 是显式指定的模板参数,t code> 是传递的参数。)

现在记住引用折叠规则:(

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 的工作原理。

First, let's take a look at what std::forward does according to the standard:

§20.2.3 [forward] p2

Returns: static_cast<T&&>(t)

(Where T is the explicitly specified template parameter and t is the passed argument.)

Now remember the reference collapsing rules:

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)

(Shamelessly stolen from this answer.)

And then let's take a look at a class that wants to employ perfect forwarding:

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
};

And now an example invocation:

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'!
}

I hope this step-by-step answer helps you and others understand just how std::forward works.

≈。彩虹 2024-12-28 19:42:43

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

It works because when perfect forwarding is invoked, the type T is not the value type, it may also be a reference type.

For example:

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&
}

As such, forward can simply look at the explicit type T to see what you really passed it. Of course, the exact implementation of doing this is non-trival, if I recall, but that's where the information is.

When you refer to a named rvalue reference, then that is indeed an lvalue. However, forward detects through the means above that it is actually an rvalue, and correctly returns an rvalue to be forwarded.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文