右值引用与移动语义

简介: 右值引用与移动语义

一、左、右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以我们称之前学习的引用为左值引用。但无论左值引用还是右值引用,其实都是给对象取别名。


1.1 什么是左值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,也可以对它赋

值。左值可以出现赋值符号的左边,但右值不能出现在赋值符号左边。定义时const修饰符后的左

值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。(可以大致理解为,能够取地址的一般都为左值)

int main()
{
    //以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    //以下是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    return 0;
}

1.2 什么是右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(非左值引用返回)等等。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。


内置类型的右值引用被称为纯右值,自定义类型的右值引用被称为将亡值


int main()
{
    double x = 1.1, y = 2.2;
    //以下是常见的右值
    10;
    x + y;
    fmin(x, y);
    //以下是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    return 0;
}

1.3 右值引用特性

右值是不能取地址的,但给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。(即可以给右值引用取地址)

因为右值引用是左值

int main()
{
    //不能取字面量10的地址。
    //但是rr引用后,可以对rr取地址,也可以修改rr。
    int&& rr = 10;
    rr = 20;
    return 0;
}

1.4 move语义

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?


有些场景下,可能真的需要用右值引用去引用左值实现移动语义。当需要用右值引用去引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于<utility>头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义。


a5ebb5a0bfbe43b8b62b674db4c70a32.png


template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
    // forward _Arg as movable
    return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
    bjy::string s1("hello world");
    bjy::string s2(s1);//这里s1是左值,调用的是拷贝构造
    bjy::string s3(std::move(s1));//将s1 move处理以后,会被当成右值,调用移动构造
    //一般是不这样用的,因为我们会发现s1的资源被转移给了s3,s1被置空了。
    return 0;
}

二、左、右值引用的比较

左值引用:

1. 左值引用只能引用左值,不能引用右值

2. const左值引用既可以引用左值,也可以引用右值

int main()
{
    //左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;//ra为a的别名
    //int& ra2 = 10;//编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用:

1. 右值引用只能引用右值,不能引用左值

2. 右值引用可以move以后的左值

int main()
{
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;
    int a = 10;
    int&& r2 = a;
    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用
    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);
    return 0;
}


三、右值引用的使用场景

const左值引用既可以引用左值,也可以引用右值,那么为什么还需要右值引用呢?


af271447123e48a5b57b633b9e959834.png


左值引用看似功能已经很完善了,但是在面对下面这些情况时,却捉襟见肘。


3.1 左值引用的短板

当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。


ff2d355d82834313ac3b5421520cbc2f.png


3.2 解决方案

这个时候右值引用便可以解决这个问题了。

c3c4f687f6b14d47be7c04be62253566.png

// 拷贝构造
string(const string& s) :_str(nullptr), _size(0), _capacity(0)
{
  cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
  string tmp(s._str);
  swap(tmp);
}
// 移动构造
string(string&& s) :_str(nullptr), _size(0), _capacity(0)
{
  cout << "string(string&& s) -- 资源转移" << endl;
  swap(s);
}

利用右值引用提供了移动构造函数后,to_string函数中的str对象会被编译器识别为将亡值。之后需要发生构造时,则会自动调用移动构造函数。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造。就是窃取别人的资源来构造自己,移动构造中没有新开空间,拷贝数据,所以比拷贝构造更加高效。


与移动构造类似的还有移动赋值,也是通过右值引用来提高效率。

// 拷贝赋值
string& operator=(const string& s)
{
  cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
  string tmp(s);
  swap(tmp);
  return *this;
}
// 移动赋值
string& operator=(string&& s)
{
  cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
  swap(s);
  return *this;
}


bff4762db80f4e29b826ebe57ebc6885.png


四、移动构造与移动赋值

那么移动构造和移动赋值有什么需要注意的地方吗?


在C++98时,我们学习过C++的类中一共有6个默认成员函数(分别是构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载)。但随着C++11的更新又新增了两个默认成员函数,即移动构造函数和移动赋值重载。


注意情况

1. 若没有自主实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员则需要看这个成员是否存在移动构造,若存在就调用移动构造,不存在就调用拷贝构造。


2. 若没有自主实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员则需要看这个成员是否存在移动赋值,若存在就调用移动赋值,不存在就调用拷贝赋值。


3. 若提供了移动构造或者移动赋值中任意一个,编译器不会自动提供拷贝构造和拷贝赋值。


五、万能引用与完美转发

5.1 万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,但是引用类型就会被限制,在后续使用中都退化成了左值。所以万能引用也被称为引用折叠(即左值引用和右值引用都被折叠为左值)。

也可以换一种理解方式。在前面提到过右值引用的特性,右值引用是左值,且左值引用也是左值。所以不出意外,既能接收左值也能接收右值的万能引用也是左值。

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}
int main()
{
    PerfectForward(10); //左值引用
    int a;
    PerfectForward(a); //左值引用
    PerfectForward(std::move(a)); //左值引用
    const int b = 8;
    PerfectForward(b); //const 左值引用
    PerfectForward(std::move(b)); //const 左值引用
    return 0;
}

如果希望能够在传递过程中保持它的左值或者右值的属性, 就需要用到完美转发


5.2 完美转发

std::forward 完美转发在传参的过程中保留对象原生类型属性

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
   Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); //右值引用
    int a;
    PerfectForward(a); //左值引用
    PerfectForward(std::move(a)); //右值引用
    const int b = 8;
    PerfectForward(b); //const 左值引用
    PerfectForward(std::move(b)); //const 右值引用
    return 0;
}


使用场景

在实际开发中,某些接口函数是提供了右值引用版本的,譬如STL中vector、list等容器的插入接口。传入右值参数并被右值引用接收后,会被认为是左值,无法顺利调用到移动构造和移动赋值等函数(没有真正减少拷贝、提高效率),这时就需要使用完美转发来在传参过程中保证右值对象的属性。


目录
相关文章
|
存储 编解码 数据可视化
单细胞分析|Seurat中的跨模态整合
在单细胞基因组学中,新方法“桥接整合”允许将scATAC-seq、scDNAme等技术的数据映射到基于scRNA-seq的参考数据集,借助多组学数据作为桥梁。研究展示了如何将scATAC-seq数据集映射到人类PBMC的scRNA-seq参考,使用10x Genomics的多组学数据集。Azimuth ATAC工具提供了自动化的工作流程,支持在R和网页平台上执行桥接整合。通过加载和预处理不同数据集,映射scATAC-seq数据并进行评估,证明了映射的准确性和细胞类型预测的可靠性。此方法扩展了参考映射框架,促进了不同技术间的互操作性。
|
机器学习/深度学习 人工智能 算法
AIGC革新商业模式与用户体验
【1月更文挑战第19天】AIGC革新商业模式与用户体验
431 1
AIGC革新商业模式与用户体验
|
JSON 小程序 前端开发
五分钟上手Vant,快速搭建小程序界面样式
五分钟上手Vant,快速搭建小程序界面样式
274 0
|
SQL 存储 NoSQL
实时计算 Flink版产品使用合集之使用ParameterTool.fromArgs(args)解析参数为null,该怎么处理
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
268 2
|
10月前
|
存储 NoSQL 分布式数据库
微服务架构下的数据库设计与优化策略####
本文深入探讨了在微服务架构下,如何进行高效的数据库设计与优化,以确保系统的可扩展性、低延迟与高并发处理能力。不同于传统单一数据库模式,微服务架构要求更细粒度的服务划分,这对数据库设计提出了新的挑战。本文将从数据库分片、复制、事务管理及性能调优等方面阐述最佳实践,旨在为开发者提供一套系统性的解决方案框架。 ####
|
Java
Java正则表达式去掉非汉字字符
【5月更文挑战第11天】Java正则表达式去掉非汉字字符
277 3
|
关系型数据库 MySQL
Navicat for mysql 如何查看 (BLOB)文件
Navicat for mysql 如何查看 (BLOB)文件
521 0
|
安全 数据库
【Debian】配置aide入侵检测服务
基于debian系统。aide主要功能检测系统文件,当系统文件发生变化,如/etc/passwd文件出现差异,那么aide将会认为系统遭受入侵被增添用户
2405 0
|
Web App开发 JSON 移动开发
mPaaS常见问题之加载不到uc 内核如何解决
mPaaS(移动平台即服务,Mobile Platform as a Service)是阿里巴巴集团提供的一套移动开发解决方案,它包含了一系列移动开发、测试、监控和运营的工具和服务。以下是mPaaS常见问题的汇总,旨在帮助开发者和企业用户解决在使用mPaaS产品过程中遇到的各种挑战
280 0
|
SQL 监控 Oracle
PostgreSQL pgcenter - 采样、统计、性能诊断、profile、cli小工具
标签 PostgreSQL , pgcenter , pg_top , awr , perf insight , 等待事件 , perf , profile , 采样 , 统计信息 背景 PostgreSQL 性能诊断的方法很多: 例如: 1、函数的性能诊断,PROFILE。 《PostgreSQL 函数调试、诊断、优化 & auto_explain & plpro
2188 0