完美转发的常见形式是通过转发引用(也叫万能引用)来保留值类别,再通过 `std::forward` 将值类别转发到另一个函数,那么我们就产生了几个问题
## 参数是利用什么机制保留值类别的
众所周知,我们可以将一个函数写成左值和右值作为参数的两个版本,模板函数却可以通过转发引用来同时承接左值和右值引用,它利用的是 [[从函数调用推导模板实参#如果 P 是到无 cv 限定模板形参的右值引用(也就是转发引用)且对应函数的调用实参是左值]] 和 [[从函数调用推导模板实参#如果 P 是引用类型]] 实现的
形如以下形式的模板函数
```cpp
template<typename T>
void foo(T &&arg) {
func(forward<T>(arg));
}
int a{0};
foo(a); //左值调用
foo(0); //右值调用
```
使用右值调用时,函数形参为非 cv 限定的`转发引用`但并非左值调用,符合规则 [[从函数调用推导模板实参#如果 P 是引用类型]] ,`arg` 是 `int` 的右值引用,则 `T` 推导为 `int` ,模板函数实例化结果为
```cpp
template<>
void foo<int>(int&&);
```
使用左值调用时,符合规则 [[从函数调用推导模板实参#如果 P 是到无 cv 限定模板形参的右值引用(也就是转发引用)且对应函数的调用实参是左值]],由于 `a` 的类型是 `int`,则`T&&`会被放置为 `int` 的左值引用 `int &`, 模板函数实例化结果为
```cpp
template<>
void foo<int&>(int&);
```
## 多重引用是如何处理的
虽然我们不能直接声明出多重引用,但类型在传递和包装之中难以避免会通过 `typedef`、`using`等方式间接声明出来,多重引用直接通过一个机制来解决
![[引用折叠]]
## 为什么需要 std::forward
因为即使我们保留了参数的值类别,右值引用再去传递给其他函数时会作为 **左值表达式** 传递,好好的右值引用就变成左值了
当我们想调用一个函数的右值形式时就没有办法
但我们也不能简单的加上 `static_cast<T&&>(arg)`,因为这样的话左值也变成右值了,如果一个函数同时提供左值和右值版本,我们将只能调用左值或者右值版本,而不是根据上层传入的值类别自动决定,因此需要一个辅助函数来根据值类别转换为左值或者右值,让我们来看 **std::forward** 的实现
```cpp
_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
return static_cast<_Ty&&>(_Arg);
}
_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
```
猛一下看很常规,返回值都一样,参数一个左值引用一个右值引用,不同的是,`_Ty`是被 `remove_reference_t` 包裹的,右值引用里有一个 `static_assert`,以及一个疑问,为什么一样的返回值却能还原出原有的值类别
### remove_reference_t 的作用
为了更好的描述,我们将 forward 改为以下形式
```cpp
template<class T>
T &&forward(T &&arg) noexcept {
return static_cast<T &&>(arg);
}
template<class T>
T &&forward(T &arg) noexcept {
return static_cast<T &&>(arg);
}
```
`remove_reference_t` 是为了避免重载冲突,比如误用了类型,将右值转为了左值引用,`auto &&z = forward<int &>(0)` ,此时 `T = int&` ,代入进两个重载中引用折叠的结果分别是
```cpp
int& && -> int&
int& & -> int&
```
此时两个重载都变成了接受左值引用的形式,编译器会报错,无法将右值绑定到非常量左值引用,如果存在 `remove_reference_t` ,则写错的 `int &` 会被移除引用,引用折叠不生效,则重载的两个参数形式保持左值引用和右值引用
### 为什么右值重载需要 `static_assert`
标准库的右值重载比左值重载多了一个静态检查
```cpp
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
```
主要是为了防止如下形式的调用,将右值绑定到错误的左值类型上
```cpp
auto &&z = std::forward<int &>(std::move(x));
```
### 返回值
左值引用和右值引用的重载函数体是一样的,返回值类型看起来也是相同的,那么什么机制在其中起了作用,自然就是 [[引用折叠]]
根据引用折叠的规则,我们将任意类型都加上一层右值引用,那么右值引用的右值引用会折叠成右值引用,否则折叠为左值,从而使返回值的值类别和参数的值类别一致,完成了 **完美转发**