当前位置: 首页 > news >正文

为什么要引入右值引用

在 C++ 旧时代,经常面临“对象拷贝”带来的性能瓶颈问题。

来先看一个类,这个类内部维护了一个很大的堆内存

class MyBuffer {
public:int* data;size_t size;MyBuffer(size_t s) : size(s) {data = new int[size];}~MyBuffer() {delete[] data;}// 拷贝构造函数:执行深拷贝MyBuffer(const MyBuffer& other) {size = other.size;data = new int[size];std::copy(other.data, other.data + size, data); }
};

考虑以下常见的函数调用场景:

MyBuffer createBuffer() {MyBuffer temp(100000000); // 申请巨量内存(约 400MB)return temp;              // 返回局部对象
}int main() {// 假设编译器未开启 RVO(返回值优化)MyBuffer mainBuf = createBuffer(); MyBuffer secBuf = mainBuf;return 0;
}

在 C++98 中,这段代码经历了极其低效的过程:

  1. 创建:createBuffer 内部创建 temp,申请了 400MB 内存。
  2. 拷贝: return 时,系统发现 temp 即将销毁,于是调用拷贝构造函数,将这 400MB 数据逐字节复制给 mainBuf。(事实上是发生了2次拷贝:temp -> 临时变量 -> mainBuf
  3. 销毁: return 完成后,temp 的析构函数被调用,刚刚申请的 400MB 内存被立即释放。

这里的值得优化的地方在于: 既然 temp 作为一个局部变量注定要被销毁,我们为什么还要大费周章地“先复制一份、再销毁原件”?如果能直接把 temp 拥有的内存“转让”给 mainBuf,就像从 temp 手里接过接力棒一样,就能省去昂贵的内存申请与数据复制开销。

右值引用的诞生:从“拷贝”到“转移所有权”

面对深拷贝的性能浪费,聪明的你一定想到了:如果构造函数能直接“偷”走对方的内存指针,不就可以省去复制的性能损耗了吗?

// 理想中的“转移所有权”的构造函数
MyBuffer(????? other) noexcept {this->data = other.data;  // 1. 直接接管对方的资源this->size = other.size;other.data = nullptr;     // 2. 将对方抹除(防止对方析构时释放了我的内存)other.size = 0;
}

我们定义了新的拷贝构造函数,只复制对方的指针和大小的值。

那么问题来了,编译器如何识别什么时候该用“深拷贝”的拷贝构造函数,什么时候该用“偷”内存指针的构造函数?

int main() {MyBuffer mainBuf = createBuffer(); // 场景 1:右边是临时变量(用完就扔)MyBuffer secBuf = mainBuf;         // 场景 2:右边是持久变量(后面还要用)return 0;
}

在场景 1 中,createBuffer() 的结果是一个“临时对象”,我们可以放心大胆地“偷”它的指针。

在场景 2 中,mainBuf 是个有名字的对象,如果偷了它的资源,后续代码再访问 mainBuf 就可能会崩溃。

为了区分这两种情况,C++11 引入了​左值(Lvalue)、右值(Rvalue)​的概念。

左值 vs 右值:核心在于“地址”

  • 左值(Lvalue): 能取地址的值。它有持久的身份,通常是变量名。
  • 右值(Rvalue): 不能取地址的值。它通常是“临时”的代名词。
    • 纯右值(prvalue):10a + b。它们可能只存在于寄存器中。
    • 将亡值(xvalue): 比如 createBuffer() 返回的临时对象,它马上就会被回收。它们虽然在内存里,但生命周期即将结束。在语义上无法使用寻址指令取到将亡值的内存地址

区分左右值的​本质并不是值有没有内存地址,而是能否取到内存地址​。

右值引用:一种新的类型

为了能访问到右值, C++11 引入了右值引用这种​新的类型​,类似​指针和左值引用一样是对现有类型的一种修饰​。

引入了右值概念以后,我们就知道 createBuffer() 的结果是一个右值,

当我们把上面的构造函数 MyBuffer(????? other) 写成: MyBuffer(MyBuffer&& other) 时,它就变成了移动构造函数。匹配的实参是右值。

这样我们新写的构造函数,就叫做移动构造函数。只做浅拷贝:

右值引用有三个关键特性:

  1. ​续命:​对将亡值的右值引用,会延长将亡值的生命周期。
  2. 身份转换: 对字面值的右值引用,是会将字面值拷贝到了内存位置上,它在内存中就有了具体的位置。
  3. 自身的左值性: 右值引用是一个新的类型,右值引用变量本身是一个左值。 因为它有名字,你可以对它寻址。

std::move:赋予左值“被偷”的权利

移动语义这么好,但它目前只能自动作用于临时变量(右值)。

如果我们想让一个​持久的变量(左值)​也能享受移动带来的性能红利,该怎么办?

这时,std::move 就登场了。

std::move 的作用是将一个左值,指定为右值。这样与右值相关的函数就能起作用

为什么要“强行”移动?

最典型的场景就是两个对象的交换(Swap)。看看 C++98 中传统的写法:

// 传统的深拷贝交换:代价昂贵
void swap(MyBuffer& a, MyBuffer& b) {MyBuffer temp = a; // 1. 第一次深拷贝(a 复制给 temp)-拷贝构造函数a = b;             // 2. 第二次深拷贝(b 复制给 a)-赋值运算符b = temp;          // 3. 第三次深拷贝(temp 复制给 b)-赋值运算符
}

你会发现这种实现方式,效率和性能都极其地低-进行了3次“深拷贝”。

通过 std::move,我们可以告诉编译器:“把这个左值看作右值吧”,这样我们新写的移动拷贝构造函数、移动赋值赋值运算符将会起作用,在这里面我们只需要“偷”它的内存指针即可:

// 移动赋值运算符
MyBuffer& operator=(MyBuffer&& other) noexcept {// 1. 自赋值检查:防止 a = std::move(a) 这种情况if (this == &other) return *this;// 2. 释放自己原有的内存(因为我要接管新的了,旧的得扔掉)delete[] data;// 3. 偷走对方的资源data = other.data;size = other.size;// 4. 将对方置空other.data = nullptr;other.size = 0;return *this;
}void swap(MyBuffer& a, MyBuffer& b) {MyBuffer temp = std::move(a); // 调用移动语义的拷贝构造-进行一次“浅拷贝”a = std::move(b);            //  调用移动语义的赋值运算符-进行一次“浅拷贝”b = std::move(temp);         //  调用移动语义的赋值运算符-进行一次“浅拷贝”
}

std::move 的真面目

很多人会被 move 这个名字误导,认为它真的在执行“移动”操作。其实:

  • std::move​ 不移动任何东西。
  • 它在底层只是一个​强制类型转换​:static_cast<T&&>
  • 它的唯一作用是:把一个左值标记为右值。

完美转发(Perfect Forwarding):不丢失属性的中转站

为什么需要完美转发?模板编程中,我们经常需要编写“中间函数”,将参数转发给底层函数。

我们希望这个中转站是“透明”的:不仅要转发参数的值,还要​原封不动地保留参数的左/右值属性​。

这样底层函数如果接收到是左值的话,就可以调用形参为左值引用相关的函数

这样底层函数如果接收到是右值的话,就可以调用形参为右值引用相关的函数

看以下这个例子就属于属性退化,丢失了值的右值属性:

class DataCollector {
public:MyBuffer m_buf;template <typename T>void setBuffer(T&& buf) {// 这里的 buf 虽然类型是 T&&,但它是个有名字的变量。在编译器眼里,它是左值!m_buf = MyBuffer(buf);  // 触发深拷贝!即便外部传的是右值}
};

还记得右值引用的其中一个特性,​右值引用本身是一个左值​。此时调用 MyBuffer(buf),编译器会调用​拷贝构造函数​。 这样这个函数无论外部传入左值还是右值都意味要做一次毫无意义的内存申请与数据复制。毫无疑问这不是我们想要的效果。

为了解决这个问题,C++11 引入​两套规则+完美转发(std::forward)​,使得最终达到的效果是:

外层调用传递的是左值时,底层函数调用的是形参为左值引用的函数,

外层调用传递的是右值时,底层函数调用的是形参为右值引用的函数

使用完美转发 std::forward 的代码如下:

class DataCollector {
public:MyBuffer m_buf;template <typename T>void setBuffer(T&& buf) {// buf 如果为右值,MyBuffer 调用的是移动构造函数// buf 如果为左值,MyBuffer 调用的是拷贝构造函数m_buf = MyBuffer(std::forward<T>(buf)); }
};

std::forward<T>() 自身的规则是:

  • 如果​​ T 为引用类型​,那么 std::forward<T>() 返回的是左值引用
  • 如果 ​T 为非引用类型​,那么 std::forward<T>() 返回的是右值引用

那么下面就来看看 T 的推导规则:

  1. 万能引用(Universal Reference / Forwarding Reference)

T&& 出现在​模板推导​(template <typename T>)中时,我们叫他万能引用,因为它有一套自己的推导规则可以适配不论是右值还是左值:

  • 推导 T 类型规则为:
    • 如果你传入​右值​,T 会被推导为​非引用类型​,在我们上述的例子里:
      1. T 会被推导为 MyBuffer
      2. 因此推导出来的函数为 void setBuffer(MyBuffer &&)
    • 如果你传入​左值​,T 会被推导为​引用类型​,在我们上述的例子里,推导出来的函数为:
      1. T 会被推导为 MyBuffer&
      2. 推导出来的函数为 void setBuffer(MyBuffer& &&)
  1. 引用折叠规则(Reference Collapsing)

上述推导的情况出现了

  • void setBuffer(MyBuffer &&) 这个就是正常形参是右值引用的函数
  • void setBuffer(MyBuffer& &&) 这里出现了三个&&&的情况,很明显不符合语法。因此 c++11 定义了引用折叠逻辑: & + && 的情况等于 &,应用这个折叠逻辑,这个函数推导成了:
    • void setBuffer(MyBuffer&)

当然引用折叠规则不止 & + && = & 这一种情况,还有很多。

对这一规则的总结是:只有“&&+&&”才会保持右值属性,其他情况一律折叠为左值引用

综上两个规则:

调用 setBuffer 函数:

  1. 如果你传入​右值​,编译器推导出 T = MyBuffer 实例化出函数 void setBuffer(MyBuffer &&) , 此时 std::forward<MyBuffer> 看到 T 不是引用,返回右值,触发移动构造函数。
  2. 如果你传入​左值​,编译器推导出 T = MyBuffer& 配合引用折叠实例化出函数 void setBuffer(MyBuffer &) ,此时 std::forward<MyBuffer&> 看到 T 是引用,返回左值,触发拷贝构造函数。

总结

综上所述,C++ 右值体系是一套环环相扣的解决方案:

  1. 核心目的​:为了消除冗余的深拷贝,引入了​移动语义​(移动构造函数与移动赋值运算符)。
  2. 触发机制​:为了精确识别并触发移动,定义了​右值​、右值引用以及 std::move 转换工具。
  3. 工程适配​:为了解决右值引用在函数传递中“身份退化”的难题,通过万能引用引用折叠​规则,配合 std::forward 实现了​完美转发​。
http://www.jsqmd.com/news/294574/

相关文章:

  • 2026防撞车租赁推荐:大黄蜂机电设备有限公司,全国400城覆盖,45000余台设备供应
  • 2026年满意度调查服务推荐:深圳神秘顾客市场调查有限公司,专业第三方满意度调研实力之选
  • 2026年防水透气阀专业厂家推荐:昆山艾尤诺新材料科技,全系产品覆盖多领域应用
  • 学霸同款10个一键生成论文工具,研究生高效写作必备!
  • 2026年智能柜领域实力推荐:山东瀚岳智能科技,RFID/医疗/贵金属/工具/物料等全系智能柜解决方案
  • 2026年高杆灯/中杆灯/玉兰灯/智慧路灯/LED路灯厂家推荐:四川莱宏照明工程集团全品类供应
  • 2026七层共挤设备及农膜推荐:青州市鲁冠塑料有限公司,全系产品覆盖多领域应用
  • 2026年玻璃温室大棚建设厂家推荐:山东柏科阿姆农业科技开发有限公司,智能/连栋/全系玻璃温室大棚承建实力之选
  • 2026集装袋厂家推荐:抗老化/防水/防静电/危险品/吨袋集装袋全品类供应,实力优选
  • 2026年英语培训实力推荐:重庆康桥阳光艺术培训有限公司,剑桥/口语/零基础/青少年英语培训全覆盖
  • 2026年调蓄池真空冲洗设备推荐:青岛铭源环保科技优质装置/一体式/知名品牌全解析
  • 2026年工业硫酸生产厂家推荐:上海孟龙实业有限公司,多领域硫酸产品全系供应
  • 2026年格宾网石笼厂家推荐:安平县玖旺丝网制品有限公司,钢丝/镀锌/铅丝格宾网护岸全系供应
  • 【Django毕设源码分享】基于Python的智能停车管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 【Django毕设源码分享】基于Python的智能停车系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 基于modelscope 的本地vlm llm调用类
  • ASP.NET Core Web APP(MVC)医疗记录管理系统 - 数据库完整指南 - 详解
  • 实用指南:技术选型指南:低代码+AI如何重塑中小企业进销存系统架构
  • 2026第一次周报
  • 堆专题
  • 2026年虹口优秀的母猫绝育医院哪家靠谱,母猫绝育/猫咪体检/宠物外科/母狗绝育/宠物体检/猫咪绝育,猫咪绝育医院哪家好
  • JS函数练习题
  • 方波发生器,摆脱了 LC/RC 选频网络?
  • 2026想找优质蒸汽锅炉制造厂家?评测带你一探究竟,锅炉厂家/导热油锅炉/蒸汽锅炉,蒸汽锅炉公司找哪家
  • 2026艺术漆市场风向标:诺兰迪直销厂家值得一试,外墙艺术漆/艺术肌理漆/墙面艺术漆/诺兰迪艺术漆,艺术漆供应商选哪家
  • 冲刺Day4
  • Web学习之网络通信
  • 一文掌握 Spring AI:集成主流大模型的完整方案与思考
  • 入门篇--人工智能发展史-10-从MCP协议到AI Agent:从静态知识到动态智能,智能体的全面演进之路
  • 2026主流GEO服务商全景图谱,GEO机制深度解析与服务商选型权威指南