diff --git a/content/posts/cpp-forward.md b/content/posts/cpp-forward.md new file mode 100644 index 0000000..bc96110 --- /dev/null +++ b/content/posts/cpp-forward.md @@ -0,0 +1,147 @@ +--- +title: "C++ 中的 std::forward" +date: 2023-07-31T17:10:01+08:00 +tags: [ cpp ] +categories: [ tech ] +weight: 50 +show_comments: true +draft: false +description: "std::forward 的目的是将一个左值保持引用状态不变传递给其他函数,本博客将介绍它所解决的问题和实现详情。" +--- + +`std::forward` 的目的是将一个左值保持引用状态不变传递给其他函数,本博客将介绍它所解决的问题和实现详情。 + + + +## 前置知识 + +### 引用折叠 + +[引用折叠](https://en.cppreference.com/w/cpp/language/reference#Reference_collapsing) 表现起来像是创建了引用的引用,如指向一个左值的左引用、指向左值引用的左引用,此时引用将会折叠,最终得到一个左引用或右引用。 + +直接写 `int & &&` 是不被允许的,但是在对一个类型添加修饰时,关于引用的修饰将会按照下面的规则折叠。可以简单记为只有两个右引用才可以折叠得到右引用,其他组合只能得到左引用。 + +```cpp +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&& +``` + +### 转发引用 + +[转发引用](https://en.cppreference.com/w/cpp/language/reference#Forwarding_references) 可以接收左引用或右引用,同时能够做到保留函数参数的类别(category,如左值和右值等),它可以使用下面两种方式定义: + +1. cv-unqualified (即无 const 或 volatile 修饰)的类型模板参数(type template parameter)的右引用作为函数参数,其中类型模板参数来自于该模板函数的模板声明。 + + 理解要点: + + 1. cv-unqualified,如使用 const 修饰以后 `template void f(const T && r)` 不是转发引用。 + 2. 类型模板参数,而非“非类型模板参数(non-type template parameter)”,如 `template void f(T&&r)` 是转发引用,而 `template ` 不是。 + 3. 右引用,如 `template void f(T&r)` 不是转发引用。 + 4. 作为函数参数,如作为模板类中 `template class A { T&& r; };` 中的 r 不是转发引用。 + 5. 类型模板来自该模板函数的模板声明,如使用模板类的类型模板参数 `template class A { void f(T&&r); };` 不是转发引用。 + +2. 除花括号初始化列表之外的 `auto&&` ,本文不做展开。 + +### 一个例子 + +```cpp +template +void f(T && t) { + // do something with t +} +``` + +当上面的函数 `f` 接受一个左值(如 `int a; f(a)`)作为参数时,只需要令 `T &&` 在经过引用折叠后是一个左引用即可,因此 T 被推导为一个左引用(如 `int&`)。 + +当接受一个右值(如 `f(1)`)作为参数时,令 T 为 `int`,`T&&` 就成为 `int&&`,从而成功进行函数调用。 + +## 问题引出 + +```cpp +#include + +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 +void handle(T && t) { + A a = A(std::forward(t)); + a.do_something(); +} + +template +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` 的 [实现](https://github.com/gcc-mirror/gcc/blob/5c8b154c56a65faf64dfc5f8852e801150cb2f26/libstdc%2B%2B-v3/include/bits/move.h#L61-L87) : + +```cpp + /** + * @brief Forward an lvalue. + * @return The parameter cast to the specified type. + * + * This function is used to implement "perfect forwarding". + */ + template + _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 + _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)` 来作为调用构造函数的参数,并且从输出中可以看到它的工作符合我们的期望,结合 `std::forward` 的源码来梳理一下最后如何分别调用到拷贝构造和移动构造的: + +1. `handle` 接受 `a` 作为参数时,T 被推导为 `A&` (引用折叠)并直接作为 `std::forward` 的模板参数,使 `_Tp` 为 `A&`,于是 `std::forward` 的返回类型为 `A&` (引用折叠),因此最后调用到拷贝构造函数 +2. `handle` 接受 `A()` 作为参数时,`T` 被推导为 `A`,进而 `_Tp` 成为 `A`,进而 `std::forward` 返回类型为 `A&&`,再加上该返回值不具名,于是成功调用移动构造函数