std::forward 如何接收正确的参数

How does std::forward receive the correct argument?

本文关键字:参数 何接收 forward std      更新时间:2023-10-16

考虑:

void g(int&);
void g(int&&);
template<class T>
void f(T&& x)
{
    g(std::forward<T>(x));
}
int main()
{
    f(10);
}

由于 id-expression x 是左值,并且std::forward对左值和右值有重载,为什么调用不绑定到采用左值的std::forward重载?

template<class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;

确实绑定到std::forward取左值的重载:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

它与T == int绑定。 此函数指定返回:

static_cast<T&&>(t)

因为f T推断出int. 因此,此重载将左值int转换为 xvalue:

static_cast<int&&>(t)

因此调用g(int&&)重载。

总之,std::forward 的左值重载可能会将其参数转换为左值或右值,具体取决于调用它的T类型。

std::forward的右值重载只能转换为右值。 如果尝试调用该重载并强制转换为 lvalue,则程序格式不正确(需要编译时错误(。

所以重载 1:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

捕捉左值。

重载 2:

template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;

捕获右值(即 X 值和 pr值(。

重载 1 可以将其左值参数强制转换为左值或 x值(后者将被解释为用于重载解析目的的右值(。

重载

2 可以仅将其右值参数强制转换为 x值(出于重载解析目的,该参数将被解释为 右值(。

重载 2 适用于 N2951 中标记为"B.应将右值转发为右值"的情况。 简而言之,这种情况可以实现:

std::forward<T>(u.get());

不确定u.get()返回左值还是右值,但无论哪种方式,如果T不是左值引用类型,您都希望移动返回的值。 但是您不使用 std::move因为如果 T 左值引用类型,则您不希望从返回中移动。

我知道这听起来有点做作。 然而,N2951 在设置激励用例方面遇到了很大的麻烦,说明std::forward应该如何使用显式提供的模板参数和普通参数的隐式提供的表达式类别的所有组合。

这不是一个容易阅读的,但模板和普通参数的每种组合的基本原理是 N2951 std::forward。 当时,这在委员会中是有争议的,而且不容易出售。

std::forward的最终形式并不完全是N2951提出的。 但是,它确实通过了N2951中提供的所有六项测试。

为什么调用不绑定到采用左值的std::forward重载?

它确实这样做了,但std::forward不会推断它的模板参数,你告诉它它是什么类型,这就是魔术发生的地方。您正在将 prvalue 传递给f()以便f()推断出[T = int].然后它调用 forward 的左值重载,并且由于引用折叠,返回类型和static_cast<T&&> forward内发生的将属于 int&& 类型,从而调用void g(int&&)重载。

如果要将左值传递给f()

int x = 0;
f(x);

f()推导出[T = int&],再次调用forward的相同左值重载,但这次返回类型和static_cast<T&&>都是int&的,同样是因为引用折叠规则。然后,这将改为调用void g(int&)重载。

现场演示


霍华德已经对你关于为什么需要 forward 的右值重载的问题有一个很好的答案,但我会添加一个人为的例子来显示两个版本的运行情况。

这两个重载背后的基本思想是,在您传递给forward的表达式的结果产生右值(prvalue 或 xvalue(的情况下,将调用右值重载。

假设您有一个类型foo,该类型具有一对 ref 限定的 get() 成员函数重载。具有&&限定符的返回一个int,而另一个返回int&

struct foo
{
    int i = 42;
    int  get() && { return i; }
    int& get() &  { return i; }
};

假设f()在传递给它的任何内容上调用get()成员函数,并将其转发给g()

template<class T>
auto f(T&& t)
{
    std::cout << __PRETTY_FUNCTION__ << 'n';
    g(forward<decltype(forward<T>(t).get())>(forward<T>(t).get()));
}
foo foo1;
f(foo1);   // calls lvalue overload of forward for both calls to forward
f(std::move(foo1)); // calls lvalue overload of forward for forward<T>(t)
                    // but calls rvalue overload for outer call to forward

现场演示

std::forward 如何接收正确的参数?

为了完美的转发,作为您的代码:

template<class T>
void f(T&& x)
{
    g(std::forward<T>(x));
}

请注意,this:std::forward 需要函数参数和模板类型参数。 模板参数 T 将编码传递给 param 的参数是左值还是右值,然后转发使用它。在这种情况下,不管x引用什么,x本身就是一个左值,选择重载1,x是转发(通用(引用参数。规则如下:

  1. 如果f参数的 expr 是左值,则xT都将使用左值引用类型
  2. 如果f参数的 expr 是 右值 ,T将是非引用类型。

例如使用 gcc 源代码:

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); } //overload 1

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }// overload 2
  • 如果传递函数 f 类型的左值为 stringstring& ,则前向函数_Tp将被string&__t将被string & &等于 string&_Tp&&string& &&等于 string& .(参考资料折叠(
  • 如果传递函数 f 类型的右值为 stringstring&& ,则前向函数_Tp将被string__t string&_Tp&&将被string&&

函数有什么作用?

    如果右值到右值(重载
  • 2(或左值到左值(重载1(,没什么可做的,只是返回。
  • 如果你不小心或其他什么,将右值引导到左值,通过static_assert给你一个编译错误。(过载 2(
  • 如果你做左值到右值(重载1(,它与std::move相同,但不方便。

正如斯科特·迈耶斯(Scott Meyers(所说:

鉴于 std::move 和 std::forward 都归结为强制转换,因此 唯一的区别是 std::move 总是投射,而 std::forward 只是有时这样做,你可能会问我们是否可以 省去std::move,只在任何地方使用std::forward。从一个 纯技术角度,答案是肯定的:std::forward can do 这一切。std::move不是必需的。当然,这两个函数都不是 真的很有必要,因为我们可以在任何地方写演员表,但我 希望我们同意那会,好吧,很糟糕。

右值重载有什么用?

我不确定,它应该在你真正需要的地方使用。如霍华德·欣南特 说:

重载 2 适用于标记为"B. 应将右值转发为 右值"在 N2951 中。简而言之,这种情况可以实现:

std::forward<T>(u.get());

您不确定 you.get(( 是否返回左值或右值,但是 无论哪种方式,如果 T 不是左值引用类型,您都希望移动 返回值。但是你不使用 std::move,因为如果 T 是一个左值 引用类型,则您不想从返回中移动。

总之,它允许我们使用 T 的类型来决定是否移动。