std::forward 的主要用途是什么?它解决了哪些问题?

发布于 2024-09-16 08:48:50 字数 373 浏览 10 评论 0原文

在完美转发中,std::forward 用于将命名右值引用 t1t2 转换为未命名右值引用。这样做的目的是什么?如果我们离开 t1 & ,这将如何影响被调用的函数 inner t2 作为左值?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

In perfect forwarding, std::forward is used to convert the named rvalue references t1 and t2 to unnamed rvalue references. What is the purpose of doing that? How would that affect the called function inner if we leave t1 & t2 as lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

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

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

发布评论

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

评论(8

孤君无依 2024-09-23 08:48:50

您必须了解转发问题。您可以详细阅读整个问题,但我会总结一下。

基本上,给定表达式 E(a, b, ... , c),我们希望表达式 f(a, b, ... , c) 为相等的。在C++03中,这是不可能的。有很多尝试,但都在某些方面失败了。


最简单的是使用左值引用:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

但这无法处理临时值(右值):f(1, 2, 3);,因为它们无法绑定到左值引用。

下一次尝试可能是:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

这解决了上述问题,因为“const X& 绑定到所有内容”,包括左值和右值,但这会导致一个新问题。现在它不允许 E 具有非 const 参数:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); 
f(i, j, k); // oops! E cannot modify these

第三次尝试接受 const 引用,但随后 const_cast>constaway:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

这接受所有值,可以传递所有值,但可能导致未定义的行为:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); 
f(i, j, k); // ouch! E can modify a const object!

最终的解决方案正确处理所有内容......但代价是无法维护。您提供了 f 的重载,以及 const 和非常量的所有 组合:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N 个参数需要 2N 个组合,这是一场噩梦。我们希望自动执行此操作。

(这实际上是我们让编译器在 C++11 中为我们做的事情。)


在 C++11 中,我们有机会解决这个问题。 一种解决方案修改了现有类型的模板推导规则,但这可能会破坏大量代码。 所以我们必须另想办法。

解决方案是改用新添加的rvalue-references;我们可以在推导右值引用类型时引入新规则并创建任何所需的结果。毕竟,我们现在不可能破解代码。

如果给出对引用的引用(注意引用是一个包含性术语,既表示 T& 又表示 T&&),我们使用以下规则来计算结果类型:

“[给定]类型 TR 是对类型 T 的引用,尝试创建类型“对 cv TR 的左值引用”会创建类型“对 T 的左值引用”,而尝试创建类型“对 cv TR 的右值引用创建类型 TR。”

或者以表格形式:

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)

接下来,使用模板参数推导:如果参数是左值 A,我们为模板参数提供对 A 的左值引用。否则,我们正常推导。这提供了所谓的通用参考(术语转发参考现已成为官方参考)。

为什么这有用?因为组合起来,我们保持了跟踪类型的值类别的能力:如果它是左值,则我们有一个左值引用参数,否则我们有一个右值引用参数。

在代码中:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

最后一件事是“转发”变量的值类别。请记住,一旦进入函数内部,参数就可以作为左值传递给任何东西:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

这是不好的。 E 需要获得与我们相同的值类别!解决方案是这样的:

static_cast<T&&>(x);

这有什么作用?假设我们在 deduce 函数中,并且我们已经传递了一个左值。这意味着 TA&,因此静态转换的目标类型是 A& &&,或只是 A&。由于 x 已经是 A&,所以我们什么都不做,只剩下一个左值引用。

当我们传递一个右值时,TA,因此静态转换的目标类型是 A&&。转换结果产生一个右值表达式,它不能再传递给左值引用。我们维护了参数的值类别。

将它们放在一起为我们提供了“完美转发”:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

f 收到左值时,E 收到左值。当f收到右值时,E收到右值。完美的。


当然,我们希望摆脱丑陋的事物。 static_cast 很神秘,而且记起来很奇怪;让我们创建一个名为 forward 的实用函数,它执行相同的操作:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

You have to understand the forwarding problem. You can read the entire problem in detail, but I'll summarize.

Basically, given the expression E(a, b, ... , c), we want the expression f(a, b, ... , c) to be equivalent. In C++03, this is impossible. There are many attempts, but they all fail in some regard.


The simplest is to use an lvalue-reference:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

But this fails to handle temporary values (rvalues): f(1, 2, 3);, as those cannot be bound to an lvalue-reference.

The next attempt might be:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Which fixes the above problem because "const X& binds to everything", including both lvalues and rvalues, but this causes a new problem. It now fails to allow E to have non-const arguments:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); 
f(i, j, k); // oops! E cannot modify these

The third attempt accepts const-references, but then const_cast's the const away:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

This accepts all values, can pass on all values, but potentially leads to undefined behavior:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); 
f(i, j, k); // ouch! E can modify a const object!

A final solution handles everything correctly...at the cost of being impossible to maintain. You provide overloads of f, with all combinations of const and non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N arguments require 2N combinations, a nightmare. We'd like to do this automatically.

(This is effectively what we get the compiler to do for us in C++11.)


In C++11, we get a chance to fix this. One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code. So we have to find another way.

The solution is to instead use the newly added rvalue-references; we can introduce new rules when deducing rvalue-reference types and create any desired result. After all, we cannot possibly break code now.

If given a reference to a reference (note reference is an encompassing term meaning both T& and T&&), we use the following rule to figure out the resulting type:

"[given] a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR."

Or in tabular form:

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)

Next, with template argument deduction: if an argument is an lvalue A, we supply the template argument with an lvalue reference to A. Otherwise, we deduce normally. This gives so-called universal references (the term forwarding reference is now the official one).

Why is this useful? Because combined we maintain the ability to keep track of the value category of a type: if it was an lvalue, we have an lvalue-reference parameter, otherwise we have an rvalue-reference parameter.

In code:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

The last thing is to "forward" the value category of the variable. Keep in mind, once inside the function the parameter could be passed as an lvalue to anything:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

That's no good. E needs to get the same kind of value-category that we got! The solution is this:

static_cast<T&&>(x);

What does this do? Consider we're inside the deduce function, and we've been passed an lvalue. This means T is a A&, and so the target type for the static cast is A& &&, or just A&. Since x is already an A&, we do nothing and are left with an lvalue reference.

When we've been passed an rvalue, T is A, so the target type for the static cast is A&&. The cast results in an rvalue expression, which can no longer be passed to an lvalue reference. We've maintained the value category of the parameter.

Putting these together gives us "perfect forwarding":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

When f receives an lvalue, E gets an lvalue. When f receives an rvalue, E gets an rvalue. Perfect.


And of course, we want to get rid of the ugly. static_cast<T&&> is cryptic and weird to remember; let's instead make a utility function called forward, which does the same thing:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
思念绕指尖 2024-09-23 08:48:50

我认为有一个实现 std::forward 的概念代码可以帮助理解。这是 Scott Meyers 演讲中的幻灯片 An effective C++11/ 14 Sampler

conceptual code Implementing std::forward

代码中的函数movestd::move。在那次演讲的前面有一个(有效的)实现。我发现 libstdc++ 中 std::forward 的实际实现,在文件 move.h 中,但它根本没有指导意义。

从用户的角度来看,它的含义是 std::forward 是到右值的条件转换。如果我正在编写一个函数,该函数需要参数中的左值或右值,并且仅当它作为右值传入时才希望将其作为右值传递给另一个函数,那么它会很有用。如果我没有将参数包装在 std::forward 中,它将始终作为普通引用传递。

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

果然,它打印了

std::string& version
std::string&& version

The code is based on an example from the previous提到的演讲。幻灯片 10,从开始的 15:00 左右开始。

I think to have a conceptual code implementing std::forward can help with the understanding. This is a slide from Scott Meyers talk An Effective C++11/14 Sampler

conceptual code implementing std::forward

Function move in the code is std::move. There is a (working) implementation for it earlier in that talk. I found actual implementation of std::forward in libstdc++, in file move.h, but it is not at all instructive.

From a user's perspective, the meaning of it is that std::forward is a conditional cast to an rvalue. It can be useful if I am writing a function which expects either an lvalue or rvalue in a parameter and wants to pass it to another function as an rvalue only if it was passed in as an rvalue. If I did not wrap the parameter in std::forward, it would be always passed as a normal reference.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Sure enough, it prints

std::string& version
std::string&& version

The code is based on an example from the previously mentioned talk. Slide 10, at about 15:00 from the start.

反差帅 2024-09-23 08:48:50

在完美转发中,std::forward 用于将命名右值引用 t1 和 t2 转换为未命名右值引用。这样做的目的是什么?如果我们离开 t1 & ,这会如何影响被调用的函数内部? t2 作为左值?

模板 <类型名称 T1,类型名称 T2>无效外部(T1&& t1,T2&& t2) 
{
    内部(std::forward(t1), std::forward(t2));
}

如果在表达式中使用命名右值引用,它实际上是左值(因为您通过名称引用对象)。考虑以下示例:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

现在,如果我们像这样调用 outer

outer(17,29);

我们希望将 17 和 29 转发到 #2,因为 17 和 29 是整数文字,因此是右值。但由于表达式 inner(t1,t2); 中的 t1t2 是左值,因此您将调用 #1 而不是 #2 。这就是为什么我们需要使用 std::forward 将引用转回未命名引用。因此,outer 中的 t1 始终是左值表达式,而 forward(t1) 可能是右值表达式,具体取决于 T1。如果 T1 是左值引用,则后者只是左值表达式。仅当outer 的第一个参数是左值表达式时,T1 才会被推断为左值引用。

In perfect forwarding, std::forward is used to convert the named rvalue reference t1 and t2 to unnamed rvalue reference. What is the purpose of doing that? How would that effect the called function inner if we leave t1 & t2 as lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

If you use a named rvalue reference in an expression it is actually an lvalue (because you refer to the object by name). Consider the following example:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Now, if we call outer like this

outer(17,29);

we would like 17 and 29 to be forwarded to #2 because 17 and 29 are integer literals and as such rvalues. But since t1 and t2 in the expression inner(t1,t2); are lvalues, you'd be invoking #1 instead of #2. That's why we need to turn the references back into unnamed references with std::forward. So, t1 in outer is always an lvalue expression while forward<T1>(t1) may be an rvalue expression depending on T1. The latter is only an lvalue expression if T1 is an lvalue reference. And T1 is only deduced to be an lvalue reference in case the first argument to outer was an lvalue expression.

小糖芽 2024-09-23 08:48:50

如果我们离开 t1 & ,这会如何影响被调用的函数内部? t2 作为左值?

如果实例化后,T1 属于 char 类型,并且 T2 属于类,则您希望传递 t1 > 每个副本和 t2 每个 const 引用。好吧,除非 inner() 根据非 const 引用获取它们,也就是说,在这种情况下您也想这样做。

尝试编写一组 outer() 函数,在没有右值引用的情况下实现此目的,从而推导出从 inner() 类型传递参数的正确方法。我认为你需要其中的 2^2 个东西,相当大的模板元东西来推断参数,并且需要大量的时间来使所有情况都正确。

然后有人提出了一个 inner() ,它接受每个指针的参数。我认为现在是 3^2。 (或者 4^2。见鬼,我懒得去思考 const 指针是否会产生影响。)

然后想象一下你想对五个参数执行此操作。或者七个。

现在你知道为什么一些聪明的人想出了“完美转发”:它让编译器为你做这一切。

How would that affect the called function inner if we leave t1 & t2 as lvalue?

If, after instantiating, T1 is of type char, and T2 is of a class, you want to pass t1 per copy and t2 per const reference. Well, unless inner() takes them per non-const reference, that is, in which case you want to do so, too.

Try to write a set of outer() functions which implement this without rvalue references, deducing the right way to pass the arguments from inner()'s type. I think you'll need something 2^2 of them, pretty hefty template-meta stuff to deduce the arguments, and a lot of time to get this right for all cases.

And then someone comes along with an inner() that takes arguments per pointer. I think that now makes 3^2. (Or 4^2. Hell, I can't be bothered to try to think whether const pointer would make a difference.)

And then imagine you want to do this for a five parameters. Or seven.

Now you know why some bright minds came up with "perfect forwarding": It makes the compiler do all this for you.

羁绊已千年 2024-09-23 08:48:50

尚未明确的一点是 static_cast 也能正确处理 const T&
程序:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

生成:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

请注意,“f”必须是模板函数。如果它只是定义为“void f(int&& a)”,则这是行不通的。

A point that hasn't been made crystal clear is that static_cast<T&&> handles const T& properly too.
Program:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produces:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Note that 'f' has to be a template function. If it's just defined as 'void f(int&& a)' this doesn't work.

若水般的淡然安静女子 2024-09-23 08:48:50

可能值得强调的是,转发必须与具有转发/通用引用的外部方法一起使用。允许单独使用前向作为以下语句,但除了造成混乱之外没有任何好处。标准委员会可能想要禁用这种灵活性,否则为什么我们不直接使用 static_cast 呢?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

在我看来,移动和前进是设计模式,是引入 r 值引用类型后的自然结果。我们不应该假设一个方法被正确使用来命名它,除非禁止错误的使用。

It may be worthwhile to emphasize that forward has to be used in tandem with an outer method with forwarding/universal reference. Using forward by itself as the following statements is allowed, but does no good other than causing confusion. The standard committee may want to disable such flexibility otherwise why don't we just use static_cast instead?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

In my opinion, move and forward are design patterns which are natural outcomes after r-value reference type is introduced. We should not name a method assuming it is correctly used unless incorrect usage is forbidden.

り繁华旳梦境 2024-09-23 08:48:50

从另一个角度来看,在处理通用引用赋值中的右值时,可能需要按原样保留变量的类型。例如,

auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

使用 std::forward,我们确保 zx 具有完全相同的类型。

此外,std::forward 不会影响左值引用:

int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

z 仍然与 x 具有相同的类型。

因此,回到您的情况,如果内部函数有 int&int&& 两个重载,您需要传递 z 这样的变量> 作业不是y 1。

示例中的类型可以通过以下方式评估:

std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;

From another viewpoint, when dealing with rvalues in a universal reference assignment, it may be desirable to preserve the type of a variable as it is. For example

auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

Using std::forward, we ensured z exactly has the same type as x.

Moreover, std::forward doesn't affect lvalue references:

int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

Still z has the same type as x.

So, back to your case, if the inner function has two overloads for int& and int&&, you want to pass variables like z assignment not y one.

The types in the example can be assessed via:

std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;

无戏配角 2024-09-23 08:48:50

std::forward 保留变量的原始 l/r 值类型,

#include <iostream>
using namespace std;

void function2(int& num){ cout << "lvalue: " << num << endl;}
void function2(int&& num){ cout << "rvalue: " << num << endl;}

template <typename T>
void function1(T&& a) {
    function2(50);            // <- we pass "50" here, which is always a rvalue
    
    int b = 50;
    function2(b);             // <- we pass "b" here, which is always a lvalue

    function2(a);             // <- we pass "a" here, but is it a lvalue or rvalue?
                              //    with this syntax, even when "a" is passed in as a rvalue,
                              //    "function1" names it "a", it will always be a lvalue

    function2(forward<T>(a)); // <- we use "forward" to preserve its original l/r value type
    cout << endl;
}

int main() {
    function1(10);
    int c = 20;
    function1(c);
}

打印输出为:

rvalue: 50
lvalue: 50
lvalue: 10
rvalue: 10

rvalue: 50
lvalue: 50
lvalue: 20
lvalue: 20

std::forward preserves variable's original l/r value type

#include <iostream>
using namespace std;

void function2(int& num){ cout << "lvalue: " << num << endl;}
void function2(int&& num){ cout << "rvalue: " << num << endl;}

template <typename T>
void function1(T&& a) {
    function2(50);            // <- we pass "50" here, which is always a rvalue
    
    int b = 50;
    function2(b);             // <- we pass "b" here, which is always a lvalue

    function2(a);             // <- we pass "a" here, but is it a lvalue or rvalue?
                              //    with this syntax, even when "a" is passed in as a rvalue,
                              //    "function1" names it "a", it will always be a lvalue

    function2(forward<T>(a)); // <- we use "forward" to preserve its original l/r value type
    cout << endl;
}

int main() {
    function1(10);
    int c = 20;
    function1(c);
}

the print out is:

rvalue: 50
lvalue: 50
lvalue: 10
rvalue: 10

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