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更适合,而通用引用就可以做到这点,根据重载的规则,更能匹配的优先.

所以请不要对通用引用函数重载

还有什么解决办法呢?有的兄弟,有的.

  1. 对于移动成本的且总是被拷贝的可拷贝形参,按值传递.
  2. SFINAE和enalbe if
posted @ 2025-08-30 13:51  T0fV404  阅读(8)  评论(0)    收藏  举报