Leafee98's Blog

C++ 中的 std::forward

· 1575 words · 4 minutes to read
Categories: tech
Tags: cpp

std::forward 的目的是将一个左值保持引用状态不变传递给其他函数,本博客将介绍它所解决的问题和实现详情。

前置知识 🔗

引用折叠 🔗

引用折叠 表现起来像是创建了引用的引用,如指向一个左值的左引用、指向左值引用的左引用,此时引用将会折叠,最终得到一个左引用或右引用。

直接写 int & && 是不被允许的,但是在对一个类型添加修饰时,关于引用的修饰将会按照下面的规则折叠。可以简单记为只有两个右引用才可以折叠得到右引用,其他组合只能得到左引用。

typedef int&  lref;
typedef int&& rref;
int n;

lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

转发引用 🔗

转发引用 可以接收左引用或右引用,同时能够做到保留函数参数的类别(category,如左值和右值等),它可以使用下面两种方式定义:

  1. cv-unqualified (即无 const 或 volatile 修饰)的类型模板参数(type template parameter)的右引用作为函数参数,其中类型模板参数来自于该模板函数的模板声明。

    理解要点:

    1. cv-unqualified,如使用 const 修饰以后 template <typename T> void f(const T && r) 不是转发引用。
    2. 类型模板参数,而非“非类型模板参数(non-type template parameter)”,如 template <typename T> void f(T&&r) 是转发引用,而 template <int A> 不是。
    3. 右引用,如 template <typename T> void f(T&r) 不是转发引用。
    4. 作为函数参数,如作为模板类中 template <typename T> class A { T&& r; }; 中的 r 不是转发引用。
    5. 类型模板来自该模板函数的模板声明,如使用模板类的类型模板参数 template <typename T> class A { void f(T&&r); }; 不是转发引用。
  2. 除花括号初始化列表之外的 auto&& ,本文不做展开。

一个例子 🔗

template <typename T>
void f(T && t) {
    // do something with t
}

当上面的函数 f 接受一个左值(如 int a; f(a))作为参数时,只需要令 T && 在经过引用折叠后是一个左引用即可,因此 T 被推导为一个左引用(如 int&)。

当接受一个右值(如 f(1))作为参数时,令 T 为 intT&& 就成为 int&&,从而成功进行函数调用。

问题引出 🔗

#include <iostream>

class A {
public:
    A()                 {  }
    A(const A & a)      { std::cout << "copy construct" << std::endl; }
    A(A && a)           { std::cout << "move construct" << std::endl; }
    void do_something() {  }
};

template <typename T>
void handle(T && t) {
    A a = A(std::forward<T>(t));
    a.do_something();
}

template <typename T>
void handle_without_forward(T && t) {
    A a = A(t);
    a.do_something();
}

int main() {
    A a;
    handle(a);                      // copy construct
    handle(A());                    // move construct

    handle_without_forward(a);      // copy construct
    handle_without_forward(A());    // copy construct
    return 0;
}

上面的代码是一个场景举例,在不考虑 std::forward 时,我们的 handle_without_forward 接受一个左值或右值作为函数参数,但是在进入该函数以后,原来的右值 A() 具有了名字 t 而成为了左值,因而接下来也只能调用拷贝构造而非移动构造,那么如何保持 t 的右值类别去调用移动构造函数(或其他具有右值引用参数的函数)呢?这个问题在介绍 std::forward 的实现中再解答。

std::forward 的实现解析 🔗

下面是 gcc 中 std::forward实现

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value,
          "std::forward must not be used to convert an rvalue to an lvalue");
      return static_cast<_Tp&&>(__t);
    }

在两个重载实现中,模板参数 _Tp 为左引用时,经过引用折叠返回类型为左引用(第二个重载实现会由于断言而编译失败);_Tp 为右引用或非引用类型时,经过引用折叠返回类型为右引用。

两个重载实现的不同点仅为一个只接受左值而另一个只接受右值(不是类型模板参数的直接右引用,所以不能使用转发引用,所以需要主动写出来两种重载实现)。

注意 std::forward 的模板参数无法被自动推导,所以在调用时显式指明模板参数是必须的,std::forward 的返回值类型也由模板参数来决定。

解决问题 🔗

回到我们遇到的问题,在 handle 中我们使用 std::forward<T>(t) 来作为调用构造函数的参数,并且从输出中可以看到它的工作符合我们的期望,结合 std::forward 的源码来梳理一下最后如何分别调用到拷贝构造和移动构造的:

  1. handle 接受 a 作为参数时,T 被推导为 A& (引用折叠)并直接作为 std::forward 的模板参数,使 _TpA&,于是 std::forward 的返回类型为 A& (引用折叠),因此最后调用到拷贝构造函数
  2. handle 接受 A() 作为参数时,T 被推导为 A,进而 _Tp 成为 A,进而 std::forward 返回类型为 A&&,再加上该返回值不具名,于是成功调用移动构造函数

Categories