C++的右值引用和移动语义

news2024/11/24 16:53:13

1.左值和右值

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

  • 左值(lvalue):可以出现在赋值表达式左侧的值,例如变量名a、数据成员a.m、解引用表达式*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左值引用可以使用右值初始化;右值引用必须使用右值初始化
  • 如果函数的返回类型是左值引用,则函数调用表达式是左值;如果函数的返回类型是右值引用或者不是引用,则函数调用表达式是右值。
  • 左值引用和右值引用本身都是左值(这意味着右值引用可以被赋值和取地址)。

例如:

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.3节。

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()的返回类型是值类型,因此return res;会发生拷贝。假设res有10万个元素,则将其拷贝到vec的代价是很高的。但实际上,use()永远不会使用res,因为res在函数fill()返回后就会被销毁,因此从resvec的拷贝就是不必要的——可以设法让vec直接复用res的资源。

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

移动

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

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

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有移动构造函数

注:C++17引入了拷贝消除(copy elision)/返回值优化(return value optimization, RVO)特性:如果初始值是纯右值(prvalue),则移动构造函数调用会被优化掉。

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

simple_vector.cpp为简化的vector定义了移动构造函数和移动赋值运算符。再次考虑前面的例子,在fill()返回时移动构造函数将被隐式调用(fill()use()的代码均不需要修改)。

3.3 std::move

前面提到,右值引用不能绑定到左值,因此左值不能被移动。但是,标准库头文件<utility>提供了std::move()函数,作用是将参数“当作”右值,从而可以将其赋给右值引用。

如果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的移动构造函数。
  • 如果一个左值出现在return语句中,则它是可移动的(move-eligible),因此不需要显式使用std::move()。例如3.1节中的fill()函数。

3.4 示例

下面是一个测试示例:

#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有移动构造函数
  • C a = f();调用移动构造函数(将返回值临时对象移动到a),因为f()是一个右值
  • C b = a;调用拷贝构造函数,因为a是一个左值
  • a = C();调用移动赋值,因为C()是一个右值
  • b = a;调用拷贝赋值,因为a是一个左值
  • b = std::move(a);调用移动赋值,因为std::move(a)是一个右值

注:

  • 前两个移动构造函数调用可能会被编译器的拷贝消除特性优化掉(除非编译器使用的C++标准版本小于C++17且指定了-fno-elide-constructors选项),从而ca的地址是一样的,整个语句只有一次默认构造函数调用。
  • 从C++17开始编译器会强制进行拷贝消除。

4.总结

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

参考

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

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

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

相关文章

十七、多线程(下)

文章目录 一、线程互斥&#xff0c;它是对的&#xff0c;但是不合理&#xff08;饥饿问题&#xff09;——同步二、条件变量&#xff08;一&#xff09;概念&#xff08;二&#xff09;条件变量接口1. pthread_cond_init 创建条件变量2. pthread_cond_wait 等待条件满足3. pthr…

类的成员之:构造器(构造方法)

1.构造器的特征&#xff1a; 它具有与类相同的名称它不声明返回值类型。&#xff08;与声明为void不同&#xff09;不能被static、final、synchronized、abstract、native修饰&#xff0c;不能有return语句返回值 2.构造器的作用&#xff1a; 1.创建对象2.初始化对象的…

StackLLaMA: A hands-on guide to train LLaMA with RLHF

Paper name StackLLaMA: A hands-on guide to train LLaMA with RLHF Paper Reading Note Project URL: https://huggingface.co/blog/stackllama Code URL: https://huggingface.co/docs/trl/index TL;DR Huggingface 公司开发的 RLHF 训练代码&#xff0c;已集成到 hugg…

产品设计-产品设计五要素

概念介绍 产品设计五要素分别是&#xff1a;战略层、范围层、结构层、框架层、表现层。自上而下的分析可用来分析已有的产品&#xff0c;自下而上分析则可以用来创造新的产品。下面是各个层级所包括的内容&#xff1a; 战略层&#xff1a;产品目标和用户需求&#xff08;做什…

【STL(2)】

STL&#xff08;2&#xff09; 知识点回顾函数对象函数对象理解系统的仿函数仿函数应用 容器适配器stackdeque queuepriority_queue mapmap使用插入访问下标访问的应用&#xff1a;计算文件中单词的个数 知识点回顾 在STL库中存在三个容器适配器&#xff0c;stack - queue - p…

西门子200系列PLC学习课程大纲(课程筹备中)

西门子200系列PLC学习课程大纲如下表所示&#xff0c;共106课&#xff0c;关注我&#xff0c;让你从菜鸟变大神。 第1课西门子200PLC概述S7-200 PLC新特性是什么第2课S7-200 PLC的CPU介绍第3课S7-200 PLC编程软件介绍第4课S7-200 PLC通信方式有哪些第5课S7-200 PLC显示面板介绍…

6.1——我在CSDN的创作纪念日

文章目录 ⭐前言⭐相遇CSDN⭐切换到编程赛道的契机&#x1f496; 好好的美工为什么切换编程赛道&#x1f496; 转换编程赛道的催化剂 ⭐写博客的目的——写给未来的自己&#x1f496; 初衷——为学习铺路&#x1f496; 博客是灯——照亮前行的路&#x1f496; 博客是路——互联…

wenet-基于预训练模型进行增量训练

1867-154075-0014 重中之重 run.sh脚本分析 wenet aishell脚本解析_weixin_43870390的博客-CSDN博客 一、准备工作 第一步&#xff1a;准备训练数据&#xff0c;拷贝到远程服务器 将准备好的数据文件0529_0531_dataset&#xff0c;上传到恒源云上的/hy-tmp/wenet/example…

数据结构与算法10:递归树、Trie树、B+树

目录 【递归树】 【Trie 树】 【B树】 【每日一练&#xff1a;最长公共前缀】 【递归树】 递归的思想是将大问题分解为小问题&#xff0c;然后再将小问题分解为更小的问题&#xff0c;直到问题的数据规模被分解得足够小&#xff0c;不用继续递归分解为止。如果把这个一层…

Effective第三版 中英 | 第2章 创建和销毁对象 | 用私有构造器或者枚举类型强化 Singleton 属性

文章目录 Effective第三版前言第二章 创建和销毁对象用私有构造器或者枚举类型强化 Singleton 属性 Effective第三版 前言 大家好&#xff0c;这里是 Rocky 编程日记 &#xff0c;喜欢后端架构及中间件源码&#xff0c;目前正在阅读 effective-java 书籍。同时也把自己学习该书…

如何在本地配置Github的项目--Python

如何在本地配置Github的项目 0. 引言1. 初步预览2. 配置环境2.1 环境已经给出2.2 环境未曾给出 3. 数据配置4. 依次调试5. 配置完成总结 0. 引言 Github上存在大量的代码。当下载下来后可能会存在疑惑&#xff1a;如何在本地配置对应的项目呢&#xff1f; 为了帮助新手解决这一…

【Android开发基础】购物车代码整理

文章目录 一、数据库设计二、Home界面三、购物车模块四、添加五、源代码 这个月总算忙完了&#xff0c;总算能够抽出时间来&#xff0c;认真写一下博客了。整理一下购物车的代码 一、数据库设计 基于SqLite简单设计一个数据存储逻辑 实体&#xff08;接收数据&#xff09; im…

【数据加密】古典密码Playfair

文章目录 一、引言1、主要任务2、分支3、密码体制分类4、攻击密码系统 二、普莱费厄体制1、构造字母表&#xff0c;设为密钥矩阵2、设立加密方法3、加密解密4、字典集合5、结果 一、引言 1、主要任务 解决信息的保密性和可认证问题&#xff0c;保证信息在生成、传递、处理、保…

Swin-Transformer详解

Swin-Transformer详解 0. 前言1. Swin-Transformer结构简介2. Swin-Transformer结构详解2.1 Patch Partition2.2 Patch Merging2.3 Swin Transformer Block2.3.1 W-MSA2.3.2 SW-MSA 3. 模型配置总结 0. 前言 Swin-Transformer是2021年微软研究院发表在ICCV上的一篇文章&#x…

数据的存储(浮点型)

目录 浮点型存储的规则 1.前面我们已经学过了整形在数据中的存储是以原码&#xff0c;反码&#xff0c;补码的形式在内存中存储的&#xff0c;那么浮点数是以什么样的形式存储的呢&#xff1f; 接下来我们通过一段代码来观察——> int main() {int n 9;float* p (float*…

String AOP的使用

面向切面编程&#xff0c;面向特定方法编程&#xff0c;以方法为对象&#xff0c;在不修改原方法的基础上&#xff0c;对方法进行操作扩展等&#xff0c;底层是通过动态代理实现的 使用开发步骤&#xff1a; 1、创建一个类&#xff0c;加上Aspect声明为一个AOP切面类&#xff…

2023 重新开始

感觉搞 IT 的日子最近都有点不太好过。 早上接到公司电话说今天是一个大日子。 为什么是大日子&#xff0c;相信所有人都是懂的。这次公司将会经历一次非常大的裁员&#xff0c;很不幸也在列表中。不过感觉这个好像也没有什么关系。 因为早就在意料之中的事情&#xff0c;经历…

c语言之结构体(初阶)

目录 1&#xff1a;结构体类型的声明 2&#xff1a;结构体初始化 3&#xff1a;结构体成员访问 4&#xff1a;结构体传参 1&#xff1a;结构体类型的声明 1&#xff1a;为啥要有结构体&#xff0c;因为当我们描述一个复杂对象的时候&#xff0c;可能平时我们的一个类型不能…

常见的五种排序

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

批量提取某音视频文案(二)

牙叔教程 简单易懂 之前写过一篇 批量提取某音视频文案 , 在之前的教程中, 我用的是微软的语音转文字功能, 今天我们换个方法, 使用 逗哥配音 的 文案提取 功能 准备工作 下载视频和音频 我在github找到的是这个仓库 https://github.com/Johnserf-Seed/TikTokDownload 注意一…