C++右值引用,移动语义和完美转发
右值引用,移动语义和完美转发
返回值优化
https://www.bilibili.com/video/BV15P411p7ri?p=2
https://www.bilibili.com/video/BV15P411p7ri?p=3
学习右值引用,移动语义和完美转发的前置知识(其实也可以不用)
左右值
这个极其复杂,概念极多,但是很难用上,稍微了解一下.
左值,顾名思义就是赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
右值,右边的值,是指表达式结束后就不再存在的临时对象。 而中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值,纯粹的右值,要么是纯粹的字面量,例如;要么是求值结果相当于字面量或匿名临时对象,例如。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、表达式都属于纯右值。但是字符串字面量不算右值,这是特例.
简单来说,左值,即可以用&取地址的值,右值就不可以,包括return返回的临时变量.
右值引用
使用 && 可以引用右值,这里实际上是左值了,因为可以取地址.比如int &&a = 19
,a是左值,19为右值.
常量引用const T&
左值右值都可以,对于右值来说就是生成一个临时变量,延长声明周期,避免低效拷贝.
通用引用
在有类型推导的情况下,准确来说
template<typename T>
void f(T&& param); //param是一个通用引用
auto&& var2 = var1; //var2是一个通用引用
这两种情况下,会出现通用引用,意思就是这可能是左值也可能是右值.原因就是存在类型推导.
如果类型推导不是标准的type&&
那么它就不是通用引用了.
引用坍缩规则
形参类型 | 实参类型 | 推导后形参最终类型 | 说明 |
---|---|---|---|
T& | &(左值引用) | T& | 左值引用 + 左值引用 → 左值引用 |
T& | &&(右值引用) | T& | 左值引用 + 右值引用 → 左值引用 |
T&& | &(左值引用) | T& | 右值引用 + 左值引用 → 左值引用 |
T&& | &&(右值引用) | T&& | 右值引用 + 右值引用 → 右值引用 |
移动语义
当return一个消失的值,使用&&可以直接移动地址而非拷贝两次(return a == (tmp = a;b = tmp)).当然现在编译器会为我们隐式生成右值转化,来降低开销,我们不用操心这个.
使用std::move
可以把左值变成右值,又称为将亡值,等于结束了它的生命周期,直接把所有权交出去了.这也叫做移动语义.源码上其实就是个转化,没有啥移动的.
但是注意如果类没有实现移动构造,默认调用还是拷贝构造,因为拷贝构造能接受右值(const).
当然它也有缺点:
- 没有移动操作:要移动的对象没有提供移动操作,所以移动的写法也会变成复制操作。
- 移动不会更快:要移动的对象提供的移动操作并不比复制速度更快。
- 移动不可用:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为
noexcept
。
值得一提的是,还有另一个场景,会使得移动并没有那么有效率:
- 源对象是左值:除了极少数的情况外(例如Item25),只有右值可以作为移动操作的来源。
完美转发
完美转发就是保持原本的参数类型,下面是例子
void process(const Widget& lvalArg); //处理左值
void process(Widget&& rvalArg); //处理右值
template<typename T> //用以转发param到process的模板
void logAndProcess(T&& param)
{
auto now = //获取现在时间
std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
在这里我们希望能通过重载process来分别处理左值右值,假设这里没有forward
,那么会出现什么情况呢?
param是通用引用,它指向的值会有相应类型,但是它本身是一个左值(把它当成指针).那么重载的意义就没了,都只会经过左值引用的函数.
为了解决这个问题做到完美转发,就需要forward
,用法可以看上面格式.
在以下情况会失败:
- 用花括号初始化
- 0和NULL作为空指针
- 仅有声明的static const数据成员.
- 重载函数名或者模板名
- 位域
什么时候使用?
凡是需要区分左值右值的,统一用通用引用来重载,第一代码更可读,否则每个都要写一个重载,太麻烦了;第二对于某些量(比如字符串字面量),性能更好,可以避免一次构造,原因就是它是const,数组形式不会退化.
凡是用通用引用的,统一用forward
转发;凡是用右值引用的,统一用move
移动.
注意点
通用引用看来很完美,但是有个问题,如果写了通用引用的函数后,又写了一个重载函数.我们的目标数据有可能走的是通用引用而不是重载.
比如说对于1,short明显比int更适合,而通用引用就可以做到这点,根据重载的规则,更能匹配的优先.
所以请不要对通用引用函数重载
还有什么解决办法呢?有的兄弟,有的.
- 对于移动成本的且总是被拷贝的可拷贝形参,按值传递.
- SFINAE和
enalbe if