C++11引入右值引用T&&
、移动语义std::move
,并结合std::forward
实现完美转发。
本文将围绕为什么需要右值引用,什么是右值引用、何时使用std::move
和std::forward
有何用展开。
为什么需要右值引用
C++03及之前存在右值,但并不存在右值引用。
缓慢的深拷贝严重拖垮C++程序的性能
考虑以下场景
int main() {
vector<int> b;
{
vector<int> a(1000000000);
b = a;
}
}
在我的M3-MacBook-Air的orbstack x86_64 Linux虚拟机上,执行时间如下
________________________________________________________
Executed in 2.24 secs fish external
usr time 0.75 secs 475.00 micros 0.75 secs
sys time 1.49 secs 222.00 micros 1.49 secs
而仅仅修改一行代码
b = std::move(a); // 使用a的右值引用,触发vector赋值运算符的重载,发生移动而不是拷贝
///
// vector&
// operator=(vector&& __x) noexcept(_Alloc_traits::_S_nothrow_move())
// {
// constexpr bool __move_storage =
// _Alloc_traits::_S_propagate_on_move_assign()
// || _Alloc_traits::_S_always_equal();
// _M_move_assign(std::move(__x), __bool_constant<__move_storage>());
// return *this;
// }
结果发生巨大变化,性能得到极大的提升
________________________________________________________
Executed in 815.27 millis fish external
usr time 546.29 millis 0.23 millis 546.06 millis
sys time 269.19 millis 1.16 millis 268.03 millis
此类性能提升得益于相比于拷贝型更快的转移型赋值操作符重载。
与之类似的,转移构造函数提供了比拷贝构造函数更好的性能,我们也可以自定义类似的转移函数优化性能
这些转移型函数重载接收右值引用类型的参数,相对应的,只有传入右值引用类型的参数才会触发此类函数重载。
但其实我们代码中有许多情况下会被自动优化出右值引用类型,正是如此,大量标准库的性能在C++11后得到了极大的提升,且这些提升不需要用户对自己的代码进行过多的修改。
什么是右值引用
右值引用是对右值的引用
那什么是右值
https://en.cppreference.com/w/cpp/language/value_category.html#rvalue
An rvalue expression is either prvalue or xvalue.
cppreference的定义或许有些晦涩,我们用字面意思解释下
默认情况下,字面量、表达式返回值和临时对象是最常见的三种右值
func(10); // 字面量:10是右值
a + b; // 表达式返回值:a + b是右值
func(10); // 表达式返回值:func(10)是右值
A(1, 2); // 临时对象A(1, 2)是右值
此外,使用std::move
也可以声明右值引用
A a(1, 2);
func(std::move(a)); // std::move(a)是右值引用
需要注意的是,基础类型的右值引用没有意义,不要再傻傻地move
基础类型,这里不是Rust🐶
于是,对右值的引用便是右值引用,表示为T&&
何时使用std::move
其实大部分情况的右值都是自动推导的,即三种最常见的右值,但有时我们也需要手动将左值转换为右值,这时候std::move
便有了用武之地。
std::move
的实现非常简单,仅有两行代码
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
这里的std::remove_reference
非常关键,其定义如下
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
无论值是左值还是右值,std::remove_reference
会去除值的引用部分,仅保留类型部分
因此,概括而言,std::move
对值进行类型转换,转为其类型的右值引用
所以,其实std::move
本身对值没有进行操作,只是会告诉函数这是一个右值,从而触发相关的转移函数,当没有与之对应的转移函数时,会自动降级为左值引用,触发拷贝函数。
可以认为std::move
只是给值贴上标签,便于转移函数辨认,从而触发更高效的语句
std::forward
有何用
经过上面的分析,可以注意到我们的目标是触发转移函数而不是拷贝函数,但考虑下面的代码
template <typename T>
void wrapper(T&& arg) {
callee(arg);
}
void callee(T&& arg) {
}
void callee(T& arg) {
}
不要被定义迷惑,这个wrapper
函数又可以接受左值引用,也可以接受右值引用
这涉及到一个关键概念:引用折叠
函数定义的形参类型 | 传入的实参 | 实参 |
---|---|---|
T& | T& | T& |
T&& | T& | T& |
T& | T&& | T& |
T&& | T&& | T&& |
可以注意到只有定义形参和传入实参均为右值引用时,实参才为右值引用
callee(A(1)); // 触发void callee(T& arg)
A a(1);
callee(a); // 触发void callee(T& arg)
这边是由于arg是具名变量,无法触发右值引用
机智如你或许想到这样修改wrapper
函数
template <typename T>
void wrapper(T&& arg) {
callee(std::move(arg));
}
这样引用折叠就会失效,所有情况都使用右值引用,导致某些情况的语义错误
std::forward
便应运而生
template <typename T>
void wrapper(T&& arg) {
callee(std::forward(arg));
}
std::forward
可以保留参数类型的不变性,正确触发对应的函数重载
这样,我们便实现了完美转发
你理解了吗,有问题欢迎评论区交流