C++ | 完美转发
本文仍为草稿
万能引用
引入
在上一篇有关右值引用的文章的开头, 我们提到了 n
和 1
两个类型相同的表达式在某些地方表现的不同
除了在赋值语句中, 在重载函数调用推导的时候, 也会有不同的表现
考虑以下程序
void foo(int& x){ |
编译器符合预期的把作为左值的 a
和 1
分别推导到了符合的, 相对应的重载函数上去, 但是我们删去第二个 foo
所在的那一行代码
void foo(int& x){ |
这句报错翻译过来意思就是: 不能将作为左值参数的 x
绑定到作为右值的 1
上去
这也很好理解, 右值是没有自己所表征的内存的, 并不能用于左值绑定, 这也在上一篇文章中讨论过了
如果把 foo
接受的参数类型改成 const int&
, 就可以接受右值了
void foo(const int& x){ |
这与我们当时说的 “右值本质是左值” 是一致的
避免指数级重载
考虑一个有 个参数的函数, 假如要求函数调用时, 能对其任意一个参数独立调用左值 / 右值…
我们要编写 个重载函数!!!
这听起来就不可思议, 所以在 C++11 中引入了万能引用来避免这个事情的发生
顾名思义, 万能引用就是一种既能支持左值, 也能支持右值引用的引用, 标准格式如下
template<typename T> |
这样的模板函数, 就能同时接受左值引用和右值引用作为参数, 我们称这样的行为为万能引用
甄别
接下来我们当一下语法警察, 对一些若是若非的 “万能引用” 作甄别
依然考虑之前给出的模板代码, 假如我们做出以下调用行为
template<typename T> |
这 4 种调用方法都是合法的, 但是值得注意的是这 4 种调用方式中, T
的类型推导结果是不同的, 请看下面的例子:
如果我们显式的实例化模板函数, 我们会发现万能引用似乎失效了
In fact, 万能引用实际上就是编译器, 为模板函数对一个确定的引用做出合理的推导
类似的, 以下写法也不属于万能引用
-
模板为参数推导, 此时只能是右值引用
template<typename T>
void foo(std::vector<T>&& vec); -
常量右值引用
template<typename T>
void foo(const T&& x); -
模板类中的成员函数, 由于实例化模板类的时候,
T
的类型已经确定, 因此编译器在调用Foo::foo()
的时候并没有进行类型推导, 因此这也不是一个万能引用template<typename T>
struct Foo{
void foo(T&& x);
};
引用折叠
有可能你会好奇: T&&
是怎么既推导出 int&
, 又推导出 int&&
的
实际上这里发生了引用折叠, 对于引用的引用, 编译器会将其折叠为单一的引用, 具体规则如下:
template<typename T> |
C++ 允许模板类型推导出现引用的引用, 其实对于类型别名, 也是允许的
typedef int& lref; |
在这种情况下, 满足如下规则: 当且仅当右值引用的右值引用是折叠到右值引用, 其他情况均为左值引用
可变参数模板
曾几何时, 我们在学习 C语言的时候, 会好奇为什么 printf()
和 scanf()
可以接收任意多个参数, 究竟什么样的写法可以支持这个行为呢?
在 C++11 中引入了可变参数模板 – 一种可以接受任意数量参数的写法, 很好的实现了上面的功能
什么? 你问我 C语言是怎么实现的? 我不知道(嘿嘿
C++ 用形象的 ...
表示可变参数
template<typename... Args> |
这个程序定义了一个模板类, 将 typename...
的类型交由编译器自行推导, 并命名为 Args
, 同时类的构造函数接受一个常量的 Args
引用 args
作为参数
对于第 7 行的实例化, 编译器显然将 Args
推导为 int
类似的函数也可以用可变参数模板
template<typename... Args> |
可变参数模板不要求类型相同, 所以下面的写法是可以接受的
template<typename... Args> |
递归展开
void my_printf(){ |
注意我们定义的空参数函数, 这是递归边界
初始化容器展开
利用 C++11 的特性 initialize_list, 可以用参数包展开初始化一个 initialize_list, 再用这个 initialize_list 初始化容器, 之后我们就可以对这个容器做操作了
以升序输出任意长序列为例
template<typename T, typename... Args> |
这个函数通过把 args
展开, 存入 vec
的方式, 实现排序
折叠表达式展开
C++17 引入了更简洁的写法展开参数包, 我们成为折叠表达式
template<class... Args> |
关于折叠表达式的介绍可以放在之后集体介绍 C++17 特性的时候, 现在就暂作了解
同时可以用格式化类进行格式化输出
template<typename T> |
emplace_back()
C++11 为顺序容器引入了一种新的元素插入方式: emplace_back()
考虑一个可以接受多个参数构造的类 Complex
, 对于 push_back
和 emplace_back
有不同的写法
struct Complex{ |
在这里我们不深究两者的性能差距, 我们只需要关注为什么 emplace_back()
可以接受任意数量的参数?
打开 MSVC 的 vector 库源码可以发现 (已做可读性调整)
template <class T, class Alloc = allocator<T>> |
相比于 push_back
, emplace_back
多了一个模板 Args
, 这正好和我们方才讨论的可变参数模板相似
同时值得注意的是, 注释中提到了 “perfectly forwarding”, 同时下面还用到一个函数 forward
, 似乎这整个函数都是靠完美转发才能实现的, 接下来将深入介绍这个概念 – 完美转发
完美转发
auto 万能引用
在开始完美转发之前, 我们先对万能引用做一个补充
除了在函数模板中可以使用万能引用, 在变量声明的时候也可以使用万能引用, 格式如下
auto&&; |
与万能引用相似的是, auto&&
也能绑定到左值和右值引用上面
引入
完美转发解决的问题就是: 转发过程中, 引用类型丢失的问题; 换言之, 完美转发能同时转发引用和引用类型; 再换言之, 完美转发能让我们实现对左值和右值做出不一样的操作
我们的预期可以描述如下:
// to lvalue |
万能引用的引入
首先, 我们的函数就要既可以接受左值, 又可以接受右值…
这不就是万能引用吗?
很好, 既然这个问题之前已解决了, 那我们就不再赘述, 定义我们的函数如下
template<typename Arg> |
再考虑引用折叠, 先前的两个函数其实就可以分别推导为下面两个函数了
// Arg = int&, int& && = int& |
std::forward
接下来的问题就是: 我们怎么将被看作是左值的右值转回右值, 同时不改变原本就是左值的左值的?
再次应用引用折叠, 我们可以巧妙的给出下面的写法
template<typename Arg> |
接下来让我好好解释一下这个写法的工作原理
- 当
a
是左值 (也就是i
), 通过类型推导,Arg
会被推导为int&
, 在函数体里面通过引用折叠,int& &&
会被折叠成int&
, 左值i
仍然被强制类型转换为左值 - 当
a
是右值 (也就是0
), 通过类型推导,Arg
会被推导为int
, 在函数体里面,a
就会直接被转化为右值
真的是太巧妙了
为了可读性, 和 std::move
类似, 这样的 static_cast
, 我们给他一个名字: std::forward
其定义如下:
template <class T> |
可以看到, 这个函数本质上就是做了一个向 T&&
的强制类型转换, 因此我们要牢记: std::forward
实际上不转发任何东西, 就像 std::move
实际上不移动任何东西一样
这样我们可以把 foo
写成一个好看的形式:
template<typename Arg> |
写到这里才发现之前没有给出
std::move
的实现, 补一下, 做个对比
template <class T>
constexpr remove_reference_t<T>&& move(T&& arg) noexcept {
return static_cast<remove_reference_t<T>&&>(arg);
}
remove_reference_t
的作用是移除 T
的引用类型(左值还是右值)