cpp随笔——浅谈右值引用,移动语义与完美转发

右值引用

什么是右值

在cpp11中添加了一个新的类型叫做右值引用,记作&&,而在开始今天的正文之前我们先来看一下什么是左值什么是右值:

  • 左值(&):存储在内存中,有明确存储地址的数据
  • 右值(&&):临时对象,可以提供数据(不可取地址访问)

而在cpp11中我们可以将右值分为两种:

  • 纯右值:非引用返回的临时变量,比如运算表达式产生的临时变量,原始字面量以及lambda表达式等
  • 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等

什么是右值引用

右值引用本身就是对右值的引用类型,因为右值是匿名的,所以我们要通过引用来找到它,关于右值引用的使用方法可以参考下面的代码:

#include <iostream>

using namespace std;

class Test
{
    public:
     Test()
    {
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj()
{
    return Test(); // 返回一个临时对象,如果没有其他引用指向该对象,该对象将被销毁
}

int main()
{
    int value=520;  //value是左值
    // int &&a1=value;//error:右值引用无法绑定在左值上
    // Test& a2=getObj();//error:右值无法给左值引用赋值
    Test&& a3=getObj();
    const Test& a4=getObj();//常量左值引用是一个万能引用,可以接受左值,右值,常量左值与常量右值
    return 0;
}

移动语义

讲到这里大家可能有点懵逼,为什么我们要使用右值应用呢?其实道理很简单,如果一个对象拥有像堆区资源,那我们如果想复制它,那么我们就要编写拷贝构造函数与重载赋值函数来实现深拷贝,像下面这样:

#include <iostream>
#include <string.h>

using namespace std;

class Test
{
public:
    int* m_data=nullptr;
    void  alloc()
    {
        m_data=new int;
        memset(m_data,0,sizeof(int));
    }
    Test() =default;
    Test(const Test& t)
    {
        cout<<"调用拷贝构造函数"<<endl;
        if(m_data==nullptr)
        {
            alloc();
        }
        memcpy(m_data,t.m_data,sizeof(int));
    }

    Test& operator =(const Test& t)
    {
        cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
        if (this == &t)   return *this;                      // 避免自我赋值。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, t.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
        return *this;
    }

    ~Test()
    {
        if(m_data!=nullptr)
        {
            delete m_data;
            m_data=nullptr;
        }
    }
};

int main()
{
    Test t1;
    t1.alloc();
    *t1.m_data=3;
    Test t3(t1);
    Test t2;
    t2=t1;
    return 0;
}

但是每次深拷贝都要进行资源空间申请以及资源拷贝,当我们要拷贝的的对象只不过是一个临时对象,尤其是它即将被销毁的话,这样无疑是耗时耗力不落好,这时候我们就可以使用移动语义来解决这个问题,那什么是移动语义呢?

移动语义是C++编程语言中一种重要的概念,它旨在通过转让而非复制对象的资源来提高程序的性能和效率,特别是在处理大型对象或包含动态分配资源(如内存、文件句柄等)的对象时。移动语义核心思想利用右值引用和特殊成员函数(移动构造函数和移动赋值运算符)来实现资源的所有权转移,而不是复制资源

其实理解起来很简单,相对于左值(类似于int&)是是将引用绑定在一个可以寻址的对象上面进而直接操作,而基于右值引用实现的移动语义一般用于绑定将要被销毁的临时对象上,将该临时对象的资源所有权转移到自己这里,让这一临时对象重获新生。

而要实现移动构造函数就要实现移动构造函数与移动赋值函数,示例代码如下:

#include <iostream>
#include <string.h>

using namespace std;

class Test
{
public:
    int* m_data=nullptr;
    void  alloc()
    {
        m_data=new int;
        memset(m_data,0,sizeof(int));
    }
    Test() =default;
    Test(const Test& t)
    {
        cout<<"调用拷贝构造函数"<<endl;
        if(m_data==nullptr)
        {
            alloc();
        }
        memcpy(m_data,t.m_data,sizeof(int));
    }

    Test(Test&& t)
    {
        cout<<"调用移动构造函数"<<endl;
        if(m_data!=nullptr) delete m_data;
        m_data=t.m_data;
        t.m_data=nullptr;
    }

    Test& operator=(Test&& t)
    {
        cout<<"调用了移动赋值函数。\n";
        if(this==&t)  return *this;
        if(m_data!=nullptr)  delete m_data;
        m_data=t.m_data;
        t.m_data=nullptr;
        return *this;
    }

    Test& operator =(const Test& t)
    {
        cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
        if (this == &t)   return *this;                      // 避免自我赋值。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, t.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
        return *this;
    }

    ~Test()
    {
        if(m_data!=nullptr)
        {
            delete m_data;
            m_data=nullptr;
        }
    }
};

int main()
{
    Test t1;
    t1.alloc();
    Test t2(std::move(t1));
    auto f=[]{Test t1;t1.alloc();return t1;};  //lambda表达式,这里是作为临时对象来使用
    Test t3;
    t3=f();
    return 0;
}

拓展: 移动语义的注意点

  • std::move() 函数: 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
  • 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
  • C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
  • 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

完美转发

右值引用是独立于值的,如果我们将右值类型作为函数参数的形参,当函数内部调用其他函数时使用它就会变回左值。cpp11中,在函数模板中,我们可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。而这就是我们所说的完美转发。

C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。而在C++11中提供了std::forward()函数来实现完美转发。

// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精简之后的样子
std::forward<T>(t);

示例代码:

#include <iostream>
#include <cstring>

using namespace std;

template<typename T>

void PrintValue(T& t)
{
    cout<<"l-value"<<t<<endl;
}

template<typename T>

void PrintValue(T&& t)
{
    cout<<"r-value:"<<t<<endl;
}

template<typename T>

void testForward(T&& t)
{
    PrintValue(t);
    PrintValue(std::move(t));
    PrintValue(std::forward<T>(t));
}

int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));
}

输出为:

root@iZuf6ckztbjhtavfplgp0dZ:~/mylib/cppdemo/cpp11新特性# ./demo2
l-value520
r-value:520
r-value:520
l-value1314
r-value:1314
l-value1314
l-value1314
r-value:1314
r-value:1314
l-value1314
r-value:1314
l-value1314
l-value1314
r-value:1314
r-value:1314
  • testForward(520);函数的形参为未定引用类型T&&,实参为右值,初始化后被推导为一个右值引用
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为``右值`
  • testForward(num);函数的形参为未定引用类型T&&,实参为左值,初始化后被推导为一个左值引用
    - printValue(v);实参为左值
    • printValue(move(v));通过move将左值转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为左值引用,最终得到一个左值引用,实参为左值
  • testForward(forward<int>(num));forward的模板类型为int,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值
  • testForward(forward<int&>(num));forward的模板类型为int&,最终会得到一个左值,函数的形参为未定引用类型T&&被左值初始化后得到一个左值引用类型
    • printValue(v);实参为左值
    • printValue(move(v));通过move将左值转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为左值引用,最终得到一个左值,实参为左值
  • testForward(forward<int&&>(num));forward的模板类型为int&&,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【LocalAI】(13):LocalAI最新版本支持Stable diffusion 3,20亿参数图像更加细腻了,可以继续研究下

最新版本v2.17.1 https://github.com/mudler/LocalAI/releases Stable diffusion 3 You can use Stable diffusion 3 by installing the model in the gallery (stable-diffusion-3-medium) or by placing this YAML file in the model folder: Stable Diffusion 3 Medium 正…

Android AOSP 6(1)

mkdir android-6.0.1_r72 cd android-6.0.1_r72 2.下载相应版本的源码 先查询设备支持的版本&#xff0c;你要下载那个版本的源码&#xff0c;科学上网查看source.android.com/source/buil…。表格如下格式&#xff0c;查询对应设备的相应分支。 BuildBranchVersionSupporte…

Python与Java实现SM2互调

文章目录 一、项目背景二、环境极其依赖三、具体功能1.Python生成密钥对2.java生成密钥对3.Python加签验签4.java加签验签 四、遇到的问题五、解决方案 一、项目背景 Python对接Java接口互相SM2加签验签 二、环境极其依赖 python环境 pip3 install gmssljava环境 <depen…

字符串根据给定关键词进行高亮显示

问题 一般使用搜索引擎的时候我们会发现,搜索出来的内容都对我们搜索的关键词进行了高亮显示, 这样我们能很直观的看出是不是我们想要的结果, 最近我也遇到了类似的功能, 因为关于舆情的系统使用到了ES, 一开始心想ES本身就有支持的API实现起来不难, 但我这里的需求还不太一样…

2023-2024 学年第二学期小学数学六年级期末质量检测模拟(制作:王胤皓)(90分钟)

word效果预览&#xff1a; 一、我会填 1. 1.\hspace{0.5em} 1. 一个多位数&#xff0c;亿位上是次小的素数&#xff0c;千位上是最小的质数的立方&#xff0c;十万位是 10 10 10 和 15 15 15 的最大公约数&#xff0c;万位是最小的合数&#xff0c;十位上的数既不是质数也…

【Kubernetes】集群学习

常见的 Kubernetes 集群类型 Kubernetes 集群可以根据不同的标准进行分类&#xff0c;但通常我们根据其部署环境和用途来区分集群类型。以下是几种常见的 Kubernetes 集群类型&#xff1a; 开发集群&#xff08;Development Cluster&#xff09;&#xff1a; 用于开发和测试环…

Linux 特殊变量 $?

一. 说明 在 Linux 和其他类 Unix 系统中&#xff0c;$? 是一个特殊的变量&#xff0c;用于获取上一个命令的退出状态码。 退出状态码是一个整数值&#xff0c;通常用来表示命令的执行结果。 ⏹退出状态码的含义 0&#xff1a;命令成功执行。0以外的数字&#xff1a;命令执…

上市公司澄清公告数据库(2001-2023)

数据来源&#xff1a;中国上市公司澄清公告数据来自深交所上市公司公告板块https://www.szse.cn/disclosure/listed/notice/index.html、上交所上市公司公告板块https://www.sse.com.cn/disclosure/listedinfo/announcement/和部分受上市公司委托发布的财经媒体如新浪财经、东方…

一小时搞定Git(含盖IDEA使用)

文章目录 1. git基本概念1.1版本控制1.1.1 版本控制软件 2. 命令的使用2.1 Linux命令2.2 git基础指令2.2.1 设置用户2.2.2 初始化本地仓库2.2.3 查看本地仓库状态2.2.4 添加暂存区域2.2.5 提交本地库2.2.6 切换版本 2.3 分支操作2.3.1 分支基本操作2.3.2 合并操作2.3.4 分支开发…

想更好应对突发网络与业务问题?您需要一款“全流量”

全流量分析&#xff0c;能为我做什么&#xff1f; 在生活中遇到问题&#xff0c;我们的第一反应可能是拿出手机拍照记录&#xff0c;方便后续处理。这些问题是临时的、突发的。 流量分析&#xff0c;就是网络中的“手机”&#xff0c;针对突发的网络故障和安全事件&#xff0…

【bug】配置SpringCloudAlibaba AI的maven依赖问题

问题描述 尝鲜alibaba的ai模块&#xff0c;maven依赖一直报找不到包&#xff0c;报错如下 Unresolved dependency: org.springframework.ai:spring-ai-core:jar:0.8.1原因分析&#xff1a; 由于是按照官方文档配置的&#xff0c;所以检查了很多遍maven配置&#xff0c;加上去…

java:spring-security的简单例子

【pom.xml】 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version> </dependency> <dependency><groupId>org.springf…

飞行堡垒系列_键盘灯开关

飞行堡垒系列键盘灯可以通过键盘上的"Fn 方向键"控制 演示机型品牌型号&#xff1a;飞行堡垒8 系统版本&#xff1a;Windows 11 飞行堡垒键盘灯可通过键盘上的"Fn方向键"控制。 " Fn 下方向键 "为减弱键盘灯光&#xff0c;多按几次键盘灯就可…

如何使用AI工具进行写作

随着AI人工智能技术的飞速发展&#xff0c;AI工具已经逐渐成为学术和专业写作的得力助手。AI工具不仅可以帮助我们提高写作效率&#xff0c;还能在内容创作上提供灵感和支持。在本文中&#xff0c;小编将和大家分享如何利用AI工具提高写作效率和质量&#xff0c;并确保文章的原…

湖北民族大学2024年成人高等继续教育招生简章

湖北民族大学&#xff0c;这所承载着深厚文化底蕴和卓越教育理念的学府&#xff0c;在崭新的2024年再次敞开怀抱&#xff0c;热烈欢迎有志于深化学习、提升自我的成人学员们。今年的成人高等继续教育招生&#xff0c;不仅是学校对于终身教育理念的具体实践&#xff0c;更是为广…

java的单例集合迭代器

迭代器Iterator 根据之前的介绍我们知道&#xff0c;单例集合是由接口Collection定义的容器。Collection接口之下由定义了List接口和Set接口&#xff0c;其中List接口定义的容器的特征是有序可重复&#xff0c;而Set接口定义的容器的特征是无序不可重复的。 List接口定义的容器…

FFmpeg源码:ff_ctz / ff_ctz_c函数分析

一、ff_ctz函数的作用 ff_ctz定义在FFmpeg源码目录的libavutil/intmath.h 下&#xff1a; #ifndef ff_ctz #define ff_ctz ff_ctz_c /*** Trailing zero bit count.** param v input value. If v is 0, the result is undefined.* return the number of trailing 0-bits*/…

LeetCode 算法:翻转二叉树 c++

原题链接&#x1f517;&#xff1a;翻转二叉树 难度&#xff1a;简单⭐️ 题目 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 示例 …

英语智汇学习系统

目 录 1 软件概述 1.1 项目研究背景及意义 2 系统相关技术 2.1 HTML、WXSS、JAVASCRIPT技术 2.2 Vanilla框架 2.3 uni-app框架 2.4 MYSQL数据库 3 需求分析 3.1 可行性分析 3.2 功能需求分析 3.3 系统用户及用例分析 3.4 非功能需求分析 3.5 数据流图…

bazel :Output Directory Layout

Output Directory Layout This page covers requirements and layout for output directories. Requirements for an output directory layout: Doesn’t collide if multiple users are building on the same box.如果多个用户在同一个盒子上建造则不会发生冲突。 Support…