万能引用(Universal Reference),引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)
[TOC]
本文简单介绍万能引用(Universal Reference), 引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)的相关内容.
引言
前面的博文介绍了右值, 右值引用以及移动语义的理解. 其中有一个问题是 std::move()
如何实现入参是左值引用的情况下依旧能通过编译. 下面是 std::move
的原型. 这里需要用到 万能引用 Universal Reference 的概念.
1 | template <typename T> |
再考虑另一个问题, 既然在博文中介绍的拷贝构造函数与移动构造函数可以用左值以及右值的区别实现重载, 那遇到模板编程会怎样呢? 毕竟在 C++ 中, 不存在引用的引用, 因此A& &
的写法是无法编译的. 这里需要介绍引用折叠的规则(Reference Collapsing Rules).
万能引用
引入
缩写:
左值引用: LRef = Lvalue reference
右值引用: RRef = Rvalue reference
万能引用: URef = Universal reference
注意此处的万能引用的概念其实是 Scotter Meyers 自己创造的概念. 用来方便理解规则.
结论: type && ⇏ RRef
更具体一些:
- RRef ⇒ type
&&
- type
&&
⇏ RRef
也就是说右值引用一定是 &&
类型, 然而 &&
类型却不一定对应右值引用(充分不必要条件), 也可能对应左值引用.
因为&&
类型可以代表 2 种引用, 包括另一种引用类型 URef.
- RRef
- 只绑定右值
- 方便移动语义
- URef
- 既可以表示左值引用也可以表示右值引用.
- 既可以绑定左值也可以绑定右值, 既可以绑定 const 也可以绑定 non-const, 也就是说可以绑定所有类型
定义与规则
If a variable or parameter has declared type T&& for some deduced type T, it’s a universal reference.
只要满足条件1: 变量被声明为
&&
类型, 条件2: 需要被推断类型. 那么它就是万能引用 URef.
规则如下:
当 URef 的初始值是 T 类型的 lvalue 时, URef 变成 LRef, T&.
当 URef 的初始值是 T 类型的 rvalue 时, URef 变成 RRef, T&&.
应用场景:
函数的模板参数,形式如下:
1
2template<typename T>
void f(T&& param);auto
声明1
auto&& var = ... ;
typedef
声明decltype
声明
例子
模板参数
1
2
3
4
5
6template<typename T>
void f(T&& param); // URef: proper syntax + deduced type
Widget w;
f(w);// w 是 lvalue ⇒ URef 成为 LRef;因此得到f(Widget&)
f(std::move(w));// std::move 产生 rvalue ⇒ URef 成为 RRef; 因此得到 f(Widget&&)
f(Widget());// Widget() 产生 rvalue ⇒URef 成为 RRef; 因此得到 f(Widget&&)auto
声明1
2
3
4
5std::vector<int> v;
...
auto&& val = 10;// 10 是 rvalue ⇒ URef 成为 RRef;val 的类型是 int&&
auto&& element = v[5];// v[5] 返回类型为 int& 且 LRefs 是 左值 ⇒ v[5] 是 lvalue ⇒ URef 成为 LRef;
//因此 element’s type is int&
不是所有的 &&
类型都是万能引用
区别点在于类型的推断是否真正在进行, 例如 STL 中的 std::vector
, push_back()
函数中的 T
模板参数类型来自于 vector<T>
根本没有参数传入到 push_back()
进行推导, 因此不是 URef.
1 | template<class T, |
与之相对的是 emplace_back()
, 有参数 Args&&... args
传入, 需要进行推导, 产生的即为 Uref.
1 | template<class T, |
Uref 相关的重载
重载 URef 毫无意义, 因为 URef 本来就可以用所有的类型初始化, 无法区分重载, 并且还会导致误解.
1 | class MessedUp { |
因此有别于 LRef 与 RRef 的差异性可以导致重载, URef 需要使用完美转发 std::forward
实现输入参数的类型区分. 完美转发稍后展开.
1 | //LRef 与 RRef |
URef 书写上的好习惯
尽量使用 ParamType
的名字取代 T
, 同时在 &&
前加上空格, 方便阅读代码.
1 | template<typename ParamType> |
引用折叠
一般场景
对于不是万能引用的场景(例如不使用模板参数的函数), 我们依旧可能会遇到”引用的引用”, 怎么破? C++ 制定了引用折叠的规则. 如下:
- T& & ⇒ T&
- T&& & ⇒ T&
- T& && ⇒ T&
- T&& && ⇒ T&&
简单的来说:
- RRef-to-RRef ⇒ RRef 只有右值引用-右值引用的情况下才会推导出结果为右值引用
- LRef-to-anything ⇒ LRef 其他时候都是推导为左值引用(可以用 infectious , 传染性的来形容 LRef 的效果)
但是需要注意的是万能引用里的规则与引用折叠并不是对立的, 尤其是在模板参数推断等情形, 如下面的例子在上面万能引用的例子中也被使用. 所以看了许久我也是很疑惑, 有时候需要跳过这些概念(感觉这里的概念是起源于现象, 而不是先有抽象的设计再制定的规则).
- 模板参数
1 | template<typename T> |
auto
声明1
2
3Widget w;
auto&& v1 = w; // lvalue initializer, v1’s type is Widget&
auto&& v2 = std::move(w); // rvalue initializer, v2’s type is Widget&&typedef
声明
1 | template<typename T> |
declared
声明
decltype 的规则与上面有些不同, 这里挖个坑, 后面整理一下.
变量本身为引用类型时, 需要去除引用外衣
当变量本身为引用类型时, 需要去除引用, 只保留本身类型. 如下 lrw
左值引用, rrw
为右值引用, 但是传入函数 f()
之前会都变成 Widget
类型, 都是左值, 然后按照万能引用的规则(左值初始化为左值引用)可得 f()
的参数实际为左值引用. 除非显式约束参数为右值(使用 std::move
).
1 | template<typename T> |
小结
type
&&
⇏ RRef.
type&&
+ 类型推断 ⇒ 万能引用 URef.
URef 依据初始值转化为 LRef 或者 RRef. Lvalue ⇒ LRef, Rvalue ⇒ RRef.
不是所有的 T&&s 模板参数都是 URef.一定要注意判断是不是真的有类型推断.
不要去重载 URef.
类型推断的基础是引用折叠.
以上概念的应用场景为:function templates, auto, typedef, and decltype.
完美转发
由于 &&
不一定意味着右值引用, 如果我们的本意是希望传入右值引用, 结果折叠/万能引用等规则转化成了左值引用, 那有什么办法解决吗?分为 2 种情况考虑.
非模板函数
1 | class shape{} |
输出为:
1 | bar(shape&&) |
shape()
返回的是一个右值, 因此调用的 bar()
的重载函数是 void bar(shape&& s)
. 但是传入到内部的 foo()
的参数 s
是左值,因此调用的重载函数是 void foo(const shape&)
. 这与我们想调用 void foo(shape&&)
的本意不符.
解决办法是显示地强制转化为右值, 使用 std::move
或者是类型转换 static_cast
.
1 | foo(std::move(s)); |
模板函数
对于模板函数, 我们可以使用完美转发 std::forward
解决这个问题.
1 | void foo(const shape&) |
输出为:
1 | foo(const shape&) |
std::forward
与 std::move
的源码解析
首先看依赖的 strcut remove_reference
, 它的作用如其名, 删除引用类型返回实体类型 type.
1 | //非引用类型直接返回本身类型即可 |
然后是 std::forward
,下面是对左值引用的转发
1 | /** |
如果输入为左值引用, 由于返回为右值引用(通过 static_cast
强制转换), 所以 Tp& &&
发生引用折叠返回结果为 Tp &
仍旧为左值引用.
如果输入为右值引用, Tp&& &&
折叠结果仍旧为 Tp &&
, 仍旧为右值引用.
对右值引用的转发
1 | /** |
先判断传入参数为右值才往下进行, 然后 Tp&& &&
折叠结果仍旧为 Tp &&
, 仍旧为右值引用.
其中 std::is_lvalue_reference<_Tp>
是一个定义在 type_traits
中的模板函数用来判断是否是右值引用.
最后看一下 std::move
的源码.
1 | /** |
很直接返回 Tp
+ &&
.
参考链接/材料:
https://cloud.tencent.com/developer/article/1561681
https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers “Universal References in C++11”,Scott Meyers,2012
万能引用(Universal Reference),引用折叠(Reference Collapsing)与完美转发(Perfect Forwarding)