leafee98-blog/content/posts/cpp-forward.md
leafee98 707daff261
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
new post: cpp-forward
2023-08-01 10:51:44 +08:00

148 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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` 的目的是将一个左值保持引用状态不变传递给其他函数,本博客将介绍它所解决的问题和实现详情。
<!--more-->
## 前置知识
### 引用折叠
[引用折叠](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 <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&&` ,本文不做展开。
### 一个例子
```cpp
template <typename T>
void f(T && t) {
// do something with t
}
```
当上面的函数 `f` 接受一个左值(如 `int a; f(a)`)作为参数时,只需要令 `T &&` 在经过引用折叠后是一个左引用即可,因此 T 被推导为一个左引用(如 `int&`)。
当接受一个右值(如 `f(1)`)作为参数时,令 T 为 `int``T&&` 就成为 `int&&`,从而成功进行函数调用。
## 问题引出
```cpp
#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` 的 [实现](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<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` 的模板参数,使 `_Tp``A&`,于是 `std::forward` 的返回类型为 `A&` (引用折叠),因此最后调用到拷贝构造函数
2. `handle` 接受 `A()` 作为参数时,`T` 被推导为 `A`,进而 `_Tp` 成为 `A`,进而 `std::forward` 返回类型为 `A&&`,再加上该返回值不具名,于是成功调用移动构造函数