【C++11】三大神器之——右值、移动语义、完美转发

2023-05-16

前言

如果你还不知道C++11引入的右值、移动语义、完美转发是什么,可以阅读这篇文章;如果你已经对这些知识了如指掌,也可以看看有什么可以补充~😏

一、右值

值类别vs变量类型

在正式认识右值之前,我们要先区分值的类别和变量类型:

  • (value) 变量 (variable) 是两个独立的概念。值不一定拥有变量名(如表达式:i + j + k)。
  • 值只有类别(category) 之分,而变量只有类型(type)之分。

值类别可以被划分左值右值

那什么是左值和右值呢?左值是能被取地址、不能被移动的值。右值是表达式中间结果/函数返回值(可能拥有变量名,也可能没有)。

有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。

C++11扩展了右值的概念,将右值分为了纯右值和将亡值,但本文不作讨论。

如下的示例将帮助我们区分左值和右值:

int i = 3;       // i是左值,3是右值
int j = i+8;    // j是左值,i+8是右值
char a = getCh();   // a是左值 ,getCh()的返回值是右值(临时变量)

左值引用、右值引用、常引用

在以前的文章中,我们曾经讨论过左值引用和常引用的区别。在本篇文章中,我们需要进一步系统的了解它们三者之间的关系。

引用类型 可以分为两种:

  • 左值引用:用&符号引用左值(但不能引用右值),
  • 右值引用:用&&符号引用右值(可以移动左值)。

在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。

使用方法如下所示:

int&& a = 3;         // 3是右值,a是右值引用
int b = 8;               // b是左值
int& bb = b;			//bb是左值引用
int&& c = b + 5;   //  b+5是右值,c是右值引用
AA&& aa = getTemp();   // getTemp()的返回值是右值(临时变量)

左值引用十分常见,我们知道是给变量取个别名,但是引入右值引用的意义是什么呢?(将在下文中解答)

在上述的代码中,getTemp()的返回值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。

在下面的代码中将帮助我们区分左值引用和右值引用:

void func(T& a);//1,参数是左值
void func(T&& a);//2,参数是右值
//T类型的变量
T var;
T& rvar1 = var;//正确,rvar1是左值
T& rvar1 = T{};//错误,左值引用不能引用右值
T&& rvar2 = T{};//正确,rvar2是右值
T&& rvar2 = var;//错误,右值引用不能引用左值
T&& rvar2 = std::move(var);//正确,可以通过std::move()将左值转为右值引用

func(var);//进入1,a是左值
func(T{});//进入2,a是右值
func(rvar1);//进入1,a是左值
func(rvar2);//进入1,rvar2是右值引用但a是左值

可以看出:

  • 左值引用变量rvar1在初始化时,不能绑定右值T{}
  • 右值引用变量rvar2在初始化时,不能绑定左值var,但是可以通过std::move()左值转为右值引用
  • 在代码的最后,右值引用变量rvar2作为实参传入func中时,在作用域内是左值(已命名的右值引用是左值)

另外,C++还支持了常引用,能够同时接受左值和右值(作为常引用)。

void func(const T& a);//a是常引用

常引用和右值引用 都能接受右值的绑定,有什么区别呢?

  • 常引用可以像右值引用一样将右值的生命期延长,但它有一个缺点是,只能读不能改。

现在回到我们的问题:引入右值引用的意义是什么?

如果函数重载能够同时接受:右值引用/常引用参数,则编译器将优先重载:右值引用参数,即引入右值引用的主要目的是实现移动语义。

下面是不同值作为实参传入形参时,函数重载优先级(数字越小优先级越高):

实参/形参T&const T&T&&const T&&
左值12
常左值1
右值312
常右值21

引用折叠

在正式学习移动语义(move semantic) 完美转发std::forward() 之前,我们还要提一嘴引用折叠(reference collapsing),它是移动语义和完美转发的实现基础。

using Lref = Data&;
using Rref = Data&&;
Data data;

Lref&  r1 = data;    // r1是左值
Lref&& r2 = data;    // r2是左值
Rref&  r3 = data;    // r3是左值
Rref&& r4 = Data{};  // r4是右值

总之,只有右值引用折叠到右值引用上仍然是一个右值引用,而其他所有的引用类型之间的折叠都将变成左值引用。

二、移动语义

为什么需要移动语义

我们知道,如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。如果被拷贝的对象是临时的,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能将对象包含的资源,直接从旧对象移动到新对象,就可以节省资源申请和释放的时间。C++11新增加的移动语义(move semantic) 就是为了做到这一点(基本类型不包含资源,其移动和拷贝相同。)。

还有另一种情况:如果资源对象本身不可拷贝(如智能指针std::unique_ptr)需要定义移动构造/移动赋值函数,其原理类似。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数。我们通过实现一个简单的string类对象来说明:

class String
{
public:
    String()
    {
        cout << "String类" << this << "的构造函数" << endl;// 显示自己被调用的日志
        const char* s = "Hello C++";
        int len = strlen(s);
        _str = new char[len + 1];
        strcpy(_str, s);
    }

    String(const String& another)
    {
        cout << "String类" << this << "的拷贝构造" << endl;// 显示自己被调用的日志
        int len = strlen(another._str);//获取源对象中字符串的长度
        _str = new char[len + 1];
        strcpy(_str, another._str);// 把数据从源对象中拷贝过来
    }

    String& operator=(String& another)
    {
        cout << "String类" << this << "的拷贝赋值" << endl;// 显示自己被调用的日志
        if (this == &another)
            return *this;// 避免自我赋值
        int len = strlen(another._str);//获取源对象中字符串的长度
        _str = new char[len + 1];
        strcpy(_str, another._str);// 把数据从源对象中拷贝过来
        return *this;
    }

    String(String&& another)noexcept
    {
        cout << "String类" << this << "的移动构造" << endl;// 显示自己被调用的日志
        if(_str != nullptr)
            delete[] _str;  //如果已分配内存,先释放掉
        this->_str = another._str;// 把资源从源对象中转移过来
        another._str = nullptr;// 把源对象中的指针置空
    }

    String& operator=(String&& another)
    {
        cout << "String类" << this << "的移动赋值" << endl;// 显示自己被调用的日志
        if (this == &another)
            return *this;// 避免自我赋值
        if(_str != nullptr)
            delete[] _str; //如果已分配内存,先释放掉
        this->_str = another._str;// 把资源从源对象中转移过来
        another._str = nullptr;// 把源对象中的指针置空
        return *this;
    }

    friend ostream& operator<<(ostream& out, String& str)
    {
        out << str._str;
        return out;
    }

    ~String()
    {
        cout << "String类" << this << "的析构函数" << endl;// 显示自己被调用的日志
        if(_str != nullptr)
        {
            delete[] _str;
            _str = nullptr;
        }
    }

private:
    char* _str;
};

上述代码中,编译器会根据传入的实参的优先级(详见上文表格),来决定重载的构造函数。

  • 当实参是左值时,使用拷贝构造,拷贝源对象所有的元素。
  • 当实参是右值时,使用移动构造,将指向源对象的内存空间的指针“移动”到新对象,并将源对象的指针置空。
  • 拷贝/移动赋值函数的原理相同,在此不再过多描述。

测试用例及输出的结果:

int main()
{
    {
        String str1{};//String类0x751f9ffd60的构造函数
        cout << "str1 = " << str1 << endl;//str1 = Hello C++

        String str2{str1};//String类0x751f9ffd58的拷贝构造
        cout << "str2 = " << str2 << endl;//str2 = Hello C++
		
		//返回一个右值(临时对象)的lambda函数
        auto f = [] {String aa; return aa;}; 

        // String str3 = f();//拷贝省略
        // cout << "str3 = " << str3 << endl;

        String str{};//String类0x751f9ffd48的构造函数
        String str3{move(str)};//String类0x751f9ffd40的移动构造
        cout << "str3 = " << str3 << endl;//str3 = Hello C++

        String str4{};//String类0x751f9ffd38的构造函数
		//String类0x751f9ffd68的构造函数
        str4 = f();//String类0x751f9ffd38的移动赋值
        //String类0x751f9ffd68的析构函数
        cout << "str4 = " << str4 << endl;//str4 = Hello C++
    }
    //String类0x751f9ffd38的析构函数
    //String类0x751f9ffd40的析构函数
    //String类0x751f9ffd48的析构函数
    //String类0x751f9ffd58的析构函数
    //String类0x751f9ffd60的析构函数
    system("pause");
    return 0;
}

尽管C++11引入了移动语义,但是仍有优化的空间——与其调用一次没有意义的移动构造函数,不如让编译器直接跳过这个过程——于是就有了拷贝省略(copy elision)

移动语义和拷贝省略的区别:

  • 移动语义是语言标准提出的概念。是通过编写遵守移动语义的移动构造函数、右值限定成员函数,在逻辑上优化对象内资源的转移流程。
  • 拷贝省略是非标准(C++ 17 前)的编译器优化。跳过移动/拷贝构造函数,让编译器直接在移动后的对象内存上,构造被移动的对象。

由于拷贝省略的存在,在上述代码中,String str3 = f();会被编译器优化,为了方便演示移动构造函数,我们使用了std::move()的方法移动返回值,当然这会造成不必要的开销。

三、完美转发

阅读本节需要读者有一定的模板编程基础。

通用引用

C++11中引入了变长模板的概念,允许向模板参数里传入不同类型的不定长引用参数。由于每个类型可能是左值引用或右值引用,针对所有可能的左右值引用组合,特化所有模板 是不现实的。

如果没用通用引用的概念,那么对于一个变长模板函数,至少需要两个重载:

template <typename T, typename ...Args>
void func(T& arg, Args&...args)
{
    func(args...);//左值直接展开
}

template <typename T, typename ...Args>
void func(T&& arg, Args&&...args)
{
    func(std::move(args...));//右值需要std::move()转发
}

Scott Meyers(Effective Modern C++的作者)指出“有时候符号&& 并不一定代表右值引用,它也可能是左值引用。”

事实上,如果一个引用符号需要通过推导才能得出左右值的类型(如模板参数类型或者auto),那么这个符号就可以是左值引用或右值引用——这就是通用引用 (universal reference)

完美转发

这一点我们通过上文中的引用折叠的示例也可以得出。

基于通用引用,我们可以对上述的代码进行改进:

template <typename T, typename ...Args>
void func(T&& arg, Args&&...args)
{
    func(std::forward<Args>(args)... );
}

其中std::forward实现了针对左右值的参数,能保证被转发参数的左、右值属性不变,即完美转发(perfect forwarding)

在C++11中,完美转发支持:

  • 如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。
  • 提供了模板函数std::forward<T>(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

使用示例如下:

void fun1(int& i)//如果参数是左值
{
    cout << "左值 = " << i << endl;
}

void fun1(int&& i)//如果参数是右值
{
    cout << "右值 = " << i << endl;
}
template<class T>
void func(T&& ii)
{
    fun1(std::forward<T>(ii));
}

完美转发的语法比较简单,至于其原理本文暂时不深入研究。😝

最后

本文部分参考自文章

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++11】三大神器之——右值、移动语义、完美转发 的相关文章

  • 2288hv5超融合服务器 数码管报888

    问题现象 2288hv5超融合服务器 xff0c 前面板数码管报888 xff0c 电源灯黄灯闪烁 xff0c 开不了机 xff0c ibmc网络是通的 xff0c 但是web网页打不开 问题原因 iBMC的版本过低 xff0c iBMC在
  • 跟我一起写操作系统(二)——史上最简单的内核

    跟我一起写操作系统 二 史上最简单的内核 转载注明出处 xff1a http www cnblogs com lucasysfeng p 4847662 html 上一讲地址 xff1a http www cnblogs com lucas
  • k8s中文文档

    http www cnblogs com huangzhenyou p 8066145 html k8s概念比较多 xff0c 有什么概念的疑惑的推荐看k8s中文文档 me的环境 操作系统 xff1a centos7 docker xff1
  • 阿里云 CentOS7 安装图形化界面 。安装图形化界面看这一篇就够了。

    阿里云centos7 下执行eclipse 响应学校老师的要求安装eclipse用于与hadoop的操作 在这之前想过两种方法来解决服务器无图形化界面 xff0c 来操作eclipse 1 在主机上下载eclipse把需要编译的代码编译成j
  • 把ESXi中的虚拟机通过OVA/OVF导出的方式迁移到Proxmox 5

    一 前言 之前发现ESXi是免费的时候 xff0c 非常兴奋地把几台服务器都装上了 xff0c 用着确实还行 xff0c 但是用久了之后就发现 xff0c 很多高端功能需要进一步付费才能使用 xff0c 比如HA等 另外就是它还有很多局限性
  • PX4 ThoneFlow光流使用

    PX4官方光流介绍 xff1a PMW3901 Based Flow Sensors PX4 User Guide 与飞控连接 接线 xff1a G接GND xff1b V接3 3V xff1b T是TX接飞控的RX口 xff1b Y接地开
  • Ubuntu PX4无人机仿真环境配置

    目录 一 VM虚拟机安装ubuntu18 04 1 VMware安装 2 新建虚拟机 二 Ubuntu系统配置 1 更改软件安装源 2 安装中文输入法 三 PX4环境搭建 1 安装git 2 下载px4源码 3 安装ROS 4 安装MAVR
  • larave5安装过程分享-MAX OSX版本

    MAC上的平台是XAMPP xff0c 自带的版本低 我用的是XAMPP MAC版本 一 本地php环境配置 which php php xff0d v xff5c php xampp php PASH 61 34 xff0f applic
  • PX4二次开发 创建进程

    目录 一 创建进程 二 仿真测试 PX4官方手册 xff1a Module Template for Full Applications PX4 User Guide 编写参照PX4源码 src templates xff1a PX4 Au
  • 【Matlab】Matlab基础绘图整理

    Matlab基础绘图整理 一张图绘制多个子图在图片文本中添加希腊字母和特殊字符其他常用函数限制坐标轴范围添加坐标轴说明添加图例修改线条类型 标记修改线条粗细 一张图绘制多个子图 主要命令 xff1a figure 第几张图 subplot
  • PX4 磁罗盘干扰分析

    磁罗盘干扰分析 推力与磁场关系正常情况干扰情况与推力相关解决方法 与推力不相关 罗盘补偿操作流程获取用于分析的日志分析日志调整罗盘补偿参数 推力与磁场关系 无人机上的电机电流会干扰无人机上搭载的磁罗盘 xff0c PX4官方提供了一些方式
  • 【C++】进制

    目录 一 进制转换1 十进制转二进制2 十进制转八进制 xff08 同上 xff09 3 二进制转八进制4 二进制转十六进制5 八进制转二进制6 十六进制转二进制 二 位运算1 原码 反码 补码2 位运算符3 变换操作 一 进制转换 1 十
  • Ubuntu20.04安装ROS2+ROS2-PX4框架搭建

    目录 Ubuntu20 04安装ROS2Set localeSetup SourcesInstall ROS2 packageEnvironment setup测试 ROS2 PX4框架搭建Install PX4Install ROS2Se
  • Jetson Nano利用ROS2通过MicroDDS与PX4通讯

    目录 Jetson Nano安装Ubuntu20 04Ubuntu20 04 配置ROS2环境Pixhawk配置Jetson Nano上MicroDDS Agent配置及和pixhawk通讯 PX4在V1 14及后续版本中 xff0c 将原
  • 用速腾RS16跑LeGO-LOAM

    版权声明 xff1a 本文为博主原创文章 xff0c 遵循 CC 4 0 BY SA 版权协议 xff0c 转载请附上原文出处链接和本声明 本文链接 xff1a https blog csdn net Zed Of Zoe article
  • Visual Studio 2017环境配置MPI v9.0 并行编程环境

    目录 第一步 xff1a 下载安装mpi 官网 xff1a http www mpich org windows版官网 xff1a https msdn microsoft com en us library bb524831 v 61 v
  • 学习java基础的心得感悟

    学完java基础 xff0c 对java面向对象的思想有更加深刻的认识了 xff0c 从学习java语言概述到最后网络编程IDE的使用 xff0c 时间用了1个月零9天 xff0c 上课时间28天 xff0c 回首感觉快又感觉漫长 xff0
  • 如何使用SQL批量替换数据库特定字段中部分特定数据

    1 替换数据库特定字段中部分特定数据的SQL语句 SQL语句 xff1a update 表名 set 字段名 61 replace 字段名 原字符串 需要替换成的字符串 以将表exam major中的字段pos2019中的数据 50 替换成
  • 阿里云ubuntu16.04 server 配置方案 1 配置桌面环境

    首先为服务器配置一个桌面系统 升级一下哦 xff01 span class hljs built in sudo span apt get update span class hljs built in sudo span apt get
  • Xshell远程连接华为云服务器

    Xshell远程连接华为云服务器 一 关于华为云1 什么是云服务器2 为什么使用华为云3 我的华为云体验 二 控制台操作 1 设置密码 2 开放端口 3 切换系统 三 Xshell操作 1 下载Xshell和Xftp2 连接云服务器 一 关

随机推荐

  • 校园网网络连接反复断开又连接是什么原因?

    网络连接反复断开又连接是什么原因 xff1f 原因可能跟ARP攻击或擅自使用P2P终结者等攻击软件有关 因为校园内多个楼宇已部署防ARP攻击网络设备 xff0c 只要判断用户计算机感染ARP或使用P2P终结者 网络执法官 聚生网管等软件攻击
  • xuperchain源码分析-启动过程

    xuperchain的启动分为两个比较大的过程 xff0c 一个是节点的初始化 xff0c 另一个是挖坑的初始化
  • 通过Excel学习PID算法(一步步理解它的KP,KI,KD)

    PID原理 PID控制算是应用非常广泛的经典控制算法 但是怎么理解PID这三个参数呢 xff1f 在参考了别人的文章之后 xff0c 我还是有点一知半解 xff0c 这时候发现不自己动手算一算是很难理解PID了 xff0c 但是我又不想做这
  • 通过Excel学习PID算法(连续系统的PID)

    总结上一节 在之前 xff0c 我们用倒水的例子通俗易懂的解释了什么是PID算法 在这里先回顾一下之前的学习的内容 P表示对误差的比例系数 与目标值差多少 xff0c 就在下一次修正中加上这个误差与P的乘积 xff0c 同时会导致系统有一个
  • 原来学习是如此地苦涩

    原文链接 xff1a http blog csdn net tangl 99 article details 2047657 最近一直在忙第一篇Paper xff0c 虽然想法大致的框架成熟了 xff0c 但是还有一些细节需要完善 这几天在
  • 互联网+时代的7个引爆点(读书笔记)

    百货商场里的销售人员一直抱怨 xff0c 大家只是到自己这里来看看 xff0c 之后转身就在网上下单 从旧视角瞎看这固然是一种文体 xff0c 显示着揭示了一种新的机会 以线下体验为入口的机会 小团队精益式的迭代 xff0c 几个周期后就可
  • maperuce运算框架

    1 xff0c 概念 mapreduce 运算框架主要实现hadoop 的数据处理 xff0c 数据处理中 流经过5个节点 数据流 xff1a input gt spilt gt map gt shuffle gt reduce xff08
  • 在Python中使用print输出时,出现UnicodeEncodeError错误,错误提示为“‘gbk‘ codec can‘t encode character ‘\u2022‘ in posit

    利用chatgpt一步步解决了这个问题 xff0c 感觉ChatGPT还是太强大了 问题描述 xff1a 在Python中使用print输出时 xff0c 出现UnicodeEncodeError错误 xff0c 错误提示为 39 gbk
  • openstack一些特性资料

    Keystone RBAC nova compute Cells Bare Metal Compute 是什么东西 xff1f http wiki openstack org blueprint nova compute cells htt
  • 【神经网络和深度学习-开发案例】 第二章 神经网络结构

    神经网络和深度学习 第二章 神经网络结构 案例 xff1a 使用神经网络识别手写数字 我将介绍一个神经网络 xff0c 它可以很好地对手写的数字进行分类 为了准备这一点 xff0c 它有助于解释一些术语 xff0c 让我们可以命名一个网络的
  • 2000页kubernetes操作手册,内容详细代码清晰,小白也能看懂

    现如今 xff0c Kubernetes业务已成长为新时代的IT基础设施 xff0c 并成为高级运维工程师 架构师 后端开发工程师的必修技术栈 毫无疑问 xff0c Kubernetes是云计算发展演进的一次彻底革命性的突破 xff0c 只
  • FreeRTOS代码阅读笔记:heap_4.c

    FreeRTOS中对于内存的管理当前一共有5种实现方式 xff08 作者当前的版本是10 1 1 xff09 xff0c 均在 Source portable MemMang 下面 xff0c 这里笔记下 heap 4 c和第二种方式比较相
  • (1)touchgfx 添加时钟控件

    第一步 xff1a 新建空白模版 添加图片 xff1a 放入 链接 xff1a https pan baidu com s 1NI6LUYrTUs64Z2jZE6AAQQ 提取码 xff1a 2odw 添加控件 xff1a 位置部件属性1T
  • 【基于51】红外寻迹智能小车 - 代码篇

    文章目录 前言一 准备工作二 使用步骤1 模块化编程2 电机模块3 小车动作模块4 PWM 和定时器 中断系统5 寻迹逻辑 总结 前言 关于硬件部分可以看我上次写的帖子https blog csdn net ZER00000001 arti
  • C++关键字override

    一 什么是override override的翻译是覆盖 实际上它在C 43 43 中可以检测哪些虚函数没有被重写并报错 注 xff1a 在派生类的成员函数中使用override时 xff0c 如果基类中无此函数 xff0c 或基类中的函数
  • 邻接矩阵和邻接表

    图的概述和存储结构 xff08 一 xff09 文章目录 前言一 图的概述1 xff09 图的分类2 xff09 图的要素 二 图的存储结构三 邻接矩阵四 邻接表 前言 有一种说法是程序是由数据结构和算法组成的 xff0c 这很能体现出数据
  • 图解迪杰斯特拉(Dijkstra)最短路径算法

    往期文章目录 干货满满 xff01 最小生成树 Prim算法 最小生成树 Kruskal算法 目录 前言 一 最短路径的概念及应用 二 Dijkstra迪杰斯特拉 1 什么是Dijkstra 2 逻辑实现 总结 前言 无论是什么程序都要和数
  • Vscode配置Git+快速入门,一篇学会80%的Git操作

    前言 团队开发中经常会用到Git xff0c 能极大简化开发的流程 xff0c 而个人开发也可以利用Git管理自己的代码 同样作为一个初学者 xff0c 我在学完Git之后写下这篇文章总结个人走过的坑 xff0c 大家一起进步 Git下载和
  • 【C++11】三大神器之——智能指针

    文章目录 前言 一 智能指针的原理1 RAII机制2 简单的实现 二 智能指针的用法1 智能指针的分类2 unique ptr基本语法 3 shared ptr基本语法 4 删除器5 weak ptr 前言 一 智能指针的原理 1 RAII
  • 【C++11】三大神器之——右值、移动语义、完美转发

    前言 如果你还不知道C 43 43 11引入的右值 移动语义 完美转发是什么 xff0c 可以阅读这篇文章 xff1b 如果你已经对这些知识了如指掌 xff0c 也可以看看有什么可以补充 x1f60f 一 右值 值类别vs变量类型 在正式认