【C++】右值引用和移动语义

news2024/12/23 23:54:57

1.左值和右值

在C++中,每个表达式或者是左值,或者是右值。

  • 左值(lvalue):可以出现在赋值表达式左侧的值,例如变量名a、数据成员a.m、下标表达式a[n]、解引用表达式*p等。左值可以被赋值和取地址。
  • 右值(rvalue):只能出现在赋值表达式右侧的值,例如字面值42、算术表达式a+b、临时对象Point(3,4)、返回值是值类型的函数调用等。右值不能被赋值和取地址。

例如:

int a;
int* p = &a;  // OK, a is lvalue
*p = 42;      // OK, *p is lvalue

p = &42;      // error, 42 is rvalue
a + 1 = *p;   // error, a + 1 is rvalue

注:实际上C++标准定义了纯右值(prvalue)、将亡值(xvalue)和左值三个类别,纯右值和将亡值统称为右值,详见Value categories - cppreference。

2.左值引用和右值引用

C++的引用(reference)是一种类型,可以看作对象的别名。引用在本质上和指针一样,都是对象的地址(指针和引用的区别详见《C++程序设计原理与实践》笔记 第17章 17.9节)。

C++提供了两种类型的引用:

  • 左值引用(lvalue reference):使用&表示,T&T类型的左值引用。左值引用是最常用的引用类型,可用于在函数调用中实现传引用(pass-by-reference)语义。
  • 右值引用(rvalue reference):使用&&表示,T&&T类型的右值引用。右值引用是C++11引入的,用于实现移动语义(见第3节)。

注:左值和右值是表达式的一种属性/分类,而左值引用和右值引用是两种不同的类型,二者是完全不同的概念,但是存在一定的联系:

  • 左值引用必须使用左值初始化(即左值引用只能绑定到左值),一个例外是const左值引用可以使用右值初始化;右值引用必须使用右值初始化
  • 如果函数的返回类型是左值引用(例如vector::operator[]),则函数调用表达式是左值;如果函数的返回类型是右值引用(例如std::move())或者不是引用(例如vector::size()),则函数调用表达式是右值。
  • 左值引用和有名字的右值引用都是左值(这意味着有名字的右值引用可以被赋值和取地址)。

例如:

int a;
int& lr = a;
int* p = &lr;  // OK, lr is lvalue
lr = 42;       // OK, lr is lvalue

int& lr2 = 42;         // error, lvalue reference can't bind to rvalue
const int& clr = 42;   // OK, const lvalue reference bind to rvalue
const int* cp = &clr;  // OK, clr is lvalue

int&& rr = a + 1;
p = &rr;  // OK, rr is lvalue
++rr;     // OK, rr is lvalue

int&& rr2 = a;             // error, rvalue reference can't bind to lvalue
int&& rr3 = rr;            // error, rvalue reference can't bind to lvalue
int&& rr4 = std::move(a);  // OK, std::move(a) is rvalue

其中,std::move()函数将左值转换为右值引用,详见3.4节。

3.移动语义

为了在特定情况下避免不必要的拷贝,C++11引入了移动语义。在介绍移动语义之前,下面通过一个vector的例子说明什么情况下存在不必要的拷贝,之后介绍如何实现移动语义。

3.1 拥有资源的类

一个类可能会获取资源,例如自由存储(使用new创建的对象或数组)、文件、锁、线程、套接字等,这样的类通常具有指向资源的指针成员。

标准库vector是一个典型的拥有资源的类的例子。例如:

vector<double> age = {0.33, 22.0, 27.2, 54.2};

下图是(简化的)age示意图:

向量示意图

其中,存储元素的数组是使用new在自由存储上分配的,age对象本身仅保存了元素个数和指向该数组的指针。

拥有资源的类通常需要拷贝构造函数、拷贝赋值运算符和析构函数,以确保

  • 当对象被拷贝时,资源被正确拷贝。
  • 当对象被销毁时,资源被正确释放。

否则可能会导致内存泄露、重复释放等问题,因为拷贝的默认含义是“拷贝所有数据成员”(即浅拷贝)。关于这一点,详见《C++程序设计原理与实践》笔记 第18章 18.3.1和18.3.2节,这里不再详细介绍。

simple_vector.h给出了一个简化的vector实现,并且定义了拷贝构造函数和拷贝赋值运算符。

然而,在某些情况下会存在不必要的拷贝。下面借用《C++程序设计原理与实践》第18章中的例子:

vector<double> fill(istream& is) {
    vector<double> res;
    for (double x; is >> x;) res.push_back(x);
    return res;
}

void use() {
    vector<double> vec = fill(cin);
    // ... use vec ...
}

由于函数fill()的返回类型是值类型,因此理论上会发生两次拷贝(res→返回值临时对象→vec)。假设res有10万个元素,则拷贝代价是很高的。但实际上,use()永远不会使用res,因为res在函数fill()返回后就会被销毁,因此从resvec的拷贝就是不必要的——可以设法让vec直接复用res的资源。

为了解决这一问题,C++11引入了移动语义(move semantics):通过“窃取”资源,直接将res的资源移动(move)到vec,如下图所示:

移动

移动之后,vec将引用res的元素,而res将被置空(换句话说,移动 = “窃取”资源 = 浅拷贝+置空原指针)。

总之,移动语义是为了解决由即将被销毁的对象初始化或赋给其他对象时发生不必要的拷贝,通过“窃取”资源(移动)来避免拷贝

注:

  • 即使没有移动语义,这里的拷贝操作也可能被编译器的拷贝消除特性优化掉,但实际效果取决于具体编译器、使用的C++标准版本以及编译选项等,详见3.5节的示例。而移动语义可以保证得到一致的结果。
  • 除了使用移动语义,还有两种替代方法:

(1)传引用参数:

void fill(istream& is, vector<double>& v) {
    for (double x; is >> x;) v.push_back(x);
}

void use() {
    vector<double> vec;
    fill(cin, vec);
    // ... use vec ...
}

缺点是不能使用返回值语法,必须先声明变量。

(2)返回new创建的指针:

vector* fill(istream& is) {
    vector<double>* res = new vector<double>;
    for (double x; is >> x;) res->push_back(x);
    return res;

}
void use() {
    vector<double>* vec = fill(cin);
    // ... use vec ...
    delete vec;
}

缺点是必须记得delete这个向量。

我们希望使用返回值语法,同时避免拷贝。移动语义可以做到这一点。

3.2 移动构造函数和移动赋值

为了在C++中表达移动语义,需要定义移动构造函数(move constructor)和移动赋值(move assignment)运算符:

T(T&& v);             // move constructor
T& operator=(T&& v);  // move assignment

移动构造函数和移动赋值运算符的参数都是右值引用,因为右值正是前面提到的“即将被销毁的对象”

当使用一个右值初始化一个相同类型的对象时,移动构造函数将被调用。 包括:

  • 初始化:T a = std::move(b);T a(std::move(b));,其中bT类型
  • 函数参数传递:f(std::move(a)),其中a和函数参数都是T类型
  • 函数返回值:return a;,其中函数返回值是T类型,且T有移动构造函数

注:如果初始值是纯右值(prvalue),则移动构造函数调用可能会被拷贝消除优化掉,详见3.3节。

当对象出现在赋值表达式左侧,并且右侧是一个相同类型的右值时,移动赋值运算符将被调用。

simple_vector.cpp为简化的vector定义了移动构造函数和移动赋值运算符。

再次考虑前面的例子,在fill()返回时,vector的移动构造函数将被隐式调用(fill()use()的代码均不需要修改)。

3.3 拷贝消除

C++标准支持拷贝消除(copy elision),允许编译器在某些情况下省略拷贝构造函数和移动构造函数的调用,从而提高程序的性能。拷贝消除的规则也随着C++版本的更新而不断扩展。

从C++17开始,在下列情况下编译器会强制进行拷贝消除:

  • return语句中,操作数是与返回类型相同的纯右值。例如,T f() { return T(); }
  • 在对象初始化中,初始值是相同类型的纯右值。例如,T x = T();

在下列情况下,编译器允许但不强制进行拷贝消除:

  • return语句中,操作数是与返回类型相同的变量的名字,但不能是函数参数。这一规则称为命名返回值优化(named return value optimization, NRVO)。例如,T f() { T x; return x; }
  • 在对象初始化中,源对象是一个相同类型的无名的临时对象。当这个临时对象来自return语句时,这一规则称为返回值优化(return value optimization, RVO)。例如,T x = f();

注:上面仅列出了常见情况,完整规则详见Copy elision - cppreference。

当拷贝消除发生时,被省略的拷贝/移动构造函数的源对象(参数)和目标对象(this)将引用同一个对象。

3.4 std::move

前面提到,右值引用不能绑定到左值,因此左值不能被移动。但是,标准库头文件<utility>提供了std::move()函数,作用是将参数转换为右值引用,即将参数“当作”右值,使其变成“可移动的”。

虽然std::move()的返回类型是右值引用,但调用该函数的表达式是一个右值(准确来说是xvalue)。如果a是一个左值,则std::move(a)是一个右值,这意味着该对象被认为是“可移动的”(可能被窃取资源),因此不能再使用。例如:

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
std::cout << a.size() << ' ' << b.size() << std::endl;

将输出 “0 3”。

注:

  • 在上面的例子中,移动并不是发生在std::move(a),而是b的移动构造函数。std::move()本质上仅仅是一个static_cast
  • 如果一个左值出现在return语句中,则它是可移动的(move-eligible),因此不需要显式使用std::move()。例如3.1节中的fill()函数。

3.5 示例

下面是一个测试示例:

#include <iostream>

class C {
public:
    C() {}
    C(const C& c) { std::cout << "copy constructor\n"; }
    C(C&& c) { std::cout << "move constructor\n"; }
    C& operator=(const C& c) { std::cout << "copy assignment\n"; return *this; }
    C& operator=(C&& c) { std::cout << "move assignment\n"; return *this; }
};

C f() {
    C c;
    return c;
}

int main() {
    C a = f();
    C b = a;
    a = C();
    b = a;
    b = std::move(a);
    return 0;
}

输出如下:

move constructor
move constructor
copy constructor
move assignment
copy assignment
move assignment
  • return c;调用移动构造函数(将局部变量c移动到返回值临时对象),因为函数f()的返回类型C不是引用类型,且C有移动构造函数
  • 调用移动构造函数(将返回值临时对象移动到a),因为f()是一个右值
  • C b = a;调用拷贝构造函数,因为a是一个左值
  • a = C();调用移动赋值,因为C()是一个右值,且C有移动赋值
  • b = a;调用拷贝赋值,因为a是一个左值
  • b = std::move(a);调用移动赋值,因为std::move(a)是一个右值,且C有移动赋值

注:

  • C a = f();涉及的两次移动构造函数调用可能会被编译器的拷贝消除特性优化掉,从而ca的地址是一样的,整个语句只有一次默认构造函数调用。
  • 使用不同的C++标准版本和编译选项的情况下,C a = f();调用移动构造函数的次数如下表所示(使用的编译器是GCC 13):
C++标准版本编译选项移动构造函数调用次数
C++11-fno-elide-constructors2 (c→返回值临时对象→a)
C++110 (&c == &a)
C++17-fno-elide-constructors1 (ca)
C++170 (&c == &a)

如果C没有移动构造函数和移动复制,那么输出会变为

copy constructor
copy constructor
copy constructor
copy assignment
copy assignment
copy assignment

类似地,C a = f();涉及的两次拷贝构造函数调用可能会被编译器的拷贝消除特性优化掉:

C++标准版本编译选项拷贝构造函数调用次数
C++11-fno-elide-constructors2 (c→返回值临时对象→a)
C++110 (&c == &a)
C++17-fno-elide-constructors1 (ca)
C++170 (&c == &a)

(由此看来,有了拷贝消除,移动语义似乎变得可有可无了)

除了移动语义,右值引用还有一个重要的用途——完美转发。

4.总结

C++的值语义是万恶之源。

参考

  • Move constructor - cppreference
  • Move assignment - cppreference
  • 【C++深陷】之“左值与右值”
  • 【C++深陷】之“对象移动”
  • Understanding lvalues, rvalues and their references
  • 《C++程序设计原理与实践》笔记 第18章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/610481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jdk动态代理源码分析

jdk动态代理源码分析 前言动态代理----demo 案例jdk动态代理源码创建代理对象获取类把二进制流生成文件 jdk 动态代理的原理 前言 上一篇中我们知道动态代理的使用, Javase 专题之 静态代理和动态代理 我们只知道其中的使用,但是原理是什么? 不明白原理只知皮毛不是我们的目的…

chatgpt赋能python:Python在原图上继续画的SEO

Python在原图上继续画的SEO Python是一种高级的多范式编程语言&#xff0c;它使用简单、易于阅读的语法以及丰富和强大的数据结构使其成为工程师的首选。Python已经成为了一种非常流行的编程语言&#xff0c;它用于多种应用领域&#xff0c;包括Web开发、数据科学、机器学习、…

区间预测 | MATLAB实现基于QRCNN-LSTM卷积长短期记忆神经网络多变量时间序列区间预测

区间预测 | MATLAB实现基于QRCNN-LSTM卷积长短期记忆神经网络多变量时间序列区间预测 目录 区间预测 | MATLAB实现基于QRCNN-LSTM卷积长短期记忆神经网络多变量时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 1.Matlab实现基于QRCNN-LSTM卷积神经…

注解、原生Spring、SchemaBased三种方式实现AOP【附详细案例】

目录 一、注解配置AOP 1. 开启注解支持 2. 在类和方法加入注解 3. 测试 4. 为一个类下的所有方法统一配置切点 二、原生Spring实现AOP 1. 引入依赖 2. 编写SpringAOP通知类 3. 编写配置类bean2.xml 4 测试 三、SchemaBased实现AOP 1. 配置切面 2. 测试 往期专栏…

音视频技术开发周刊 | 296

每周一期&#xff0c;纵览音视频技术领域的干货。 新闻投稿&#xff1a;contributelivevideostack.com。 22字声明、近400名专家签署、AI教父Hinton与OpenAI CEO领头预警&#xff1a;AI可能灭绝人类&#xff01; 这份声明一经发布&#xff0c;便迅速得到了多伦多大学计算机科学…

基于zookeeper的kafka中间件

一、Zookeeper 概述 1、Zookeeper 定义 Zookeeper是一个开源的分布式的&#xff0c;为分布式框架提供协调服务的Apache项目。 2、Zookeeper 工作机制 Zookeeper从设计模式角度来理解&#xff1a;是一个基于观察者模式设计的分布式服务管理框架&#xff0c;它负责存储和管理…

昨天,小灰做了人生的第一次直播!

熟悉小灰的朋友们都知道&#xff0c;小灰是一个非常腼腆的人。虽然我比较擅长写东西&#xff0c;但完全不擅长口头表达&#xff0c;在公开场合讲话很容易紧张。 因此&#xff0c;对于网上直播&#xff0c;小灰在以前完全不敢想象。 但是&#xff0c;人终究需要成长的。就在昨天…

Disco Diffusion 快速入门

Disco Diffusion 快速入门 简介快速开始进阶使用修改prompt给定指导图像修改基础参数运行参数设置运行建议模型设置参数详情 简介 Disco Diffusion&#xff08;DD&#xff09;是一个CLIP指导的AI图像生成技术&#xff0c;简单来说&#xff0c;Diffusion是一个对图像不断去噪的…

路径规划 | 图解RRT-Connect算法(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 RRT-Connect基本原理2 RRT-Connect vs. RRT3 ROS C算法实现4 Python算法实现5 Matlab算法实现 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全局规划(图搜索、采样法、智能算法等)…

chatgpt赋能python:Python实现奇数位偶数位互换的方法

Python实现奇数位偶数位互换的方法 Python是一种高级的、面向对象的编程语言&#xff0c;在当今的编程领域中具有广泛的应用。它被用于数据分析、机器学习、Web开发等众多领域&#xff0c;其简洁的语法和强大的库被开发者们广泛使用。本文将介绍Python中奇数位偶数位互换的方法…

驱动开发:内核实现SSDT挂钩与摘钩

在前面的文章《驱动开发&#xff1a;内核解析PE结构导出表》中我们封装了两个函数KernelMapFile()函数可用来读取内核文件&#xff0c;GetAddressFromFunction()函数可用来在导出表中寻找指定函数的导出地址&#xff0c;本章将以此为基础实现对特定SSDT函数的Hook挂钩操作&…

【Django 网页Web开发】07. 快捷的表单生成 Form与MoudleForm(保姆级图文)

目录 注意 正规写法是 ModelForm&#xff0c;下面文章我多实现效果url.py新建3个html文件数据库连接model.py 数据表1. 原始方法view.pytestOrgion.html 2. Form方法view.pytestForm.html 3. MoudleForm方法给字段设置样式面向对象的思路&#xff0c;批量添加样式错误信息的显示…

ASIC-WORLD Verilog(10)编写测试脚本Testbench的艺术

写在前面 在自己准备写一些简单的verilog教程之前&#xff0c;参考了许多资料----Asic-World网站的这套verilog教程即是其一。这套教程写得极好&#xff0c;奈何没有中文&#xff0c;在下只好斗胆翻译过来&#xff08;加了自己的理解&#xff09;分享给大家。 这是网站原文&…

干货!来自北大、KAUST、斯坦福、达摩院的大模型前沿动态:表格推理、代码生成、MiniGPT-4、生成式推理...

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; ChatGPT的发布使得国内外众多的研究机构掀起了一股AI热潮&#xff0c;而这也进一步推动了人们对大语言模型的深入研究。2023年4月26日&#xff0c;AI TIME举办的大模型专场四活动邀请了阿里巴巴达摩院NLP研究员…

在 IDEA 中配置 JavaFX 11

因为从 Java8/openjdk 之后&#xff0c;javafx 从 jdk 中移除&#xff0c;如果进行 JavaFX 开发需要在 module 中添加 lib&#xff0c;并对 IDE 进行配置&#xff0c;确保 jdk 可以与 javafx 正常调用。 javafx 下载路径&#xff0c;主页网址&#xff1a;https://openjfx.io/ …

开发实践|程序员是如何刷抖音、玩快手、看头条进行赚米的?

欢迎关注「全栈工程师修炼指南」公众号 点击 &#x1f447; 下方卡片 即可关注我哟! 设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习&#xff01; “ 花开堪折直须折&#xff0c;莫待无花空折枝。 ” 作者主页&#xff1a;[ https://www.weiyigeek.top ] 博客&…

【计算机组成原理与体系结构】数据的表示与运算

目录 一、进位计数制 二、信息编码 三、定点数数据表示 四、校验码 五、定点数补码加减运算 六、标志位的生成 七、定点数的移位运算 八、定点数的乘除运算 九、浮点数的表示 十、浮点数的运算 一、进位计数制 整数部分&#xff1a; 二进制、八进制、十六进制 --…

穿越认知峡谷

十年前&#xff0c;2013 年的这个时候&#xff0c;“互联网思维”在国内大火。我没有认真研究过这件事的来龙去脉&#xff0c;不过印象里 2012 年底《罗辑思维》视频栏目的开播&#xff0c;以及差不多同时小米手机的爆发&#xff0c;对“互联网思维”的大流行应该是起了重要的推…

【ABAP】数据类型(一)「数据类型概要及分类」

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后端的开发语言A…

Nginx正则表达式、location、rewrite

目录 一、常用的Nginx正则表达式 二&#xff1a;localtion 1、location 分类 2、 location 常用的匹配规则 3、location 优先级 4、 location 示例 5、优先级总结 6、实际网站使用中&#xff0c;至少有三个匹配规则定义 &#xff08;1&#xff09;第一个必选规则 &…