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

2024-06-23 22:52

本文主要是介绍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的模板参数为右值引用,最终得到一个右值,实参为右值

这篇关于cpp随笔——浅谈右值引用,移动语义与完美转发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1088508

相关文章

Java资源管理和引用体系的使用详解

《Java资源管理和引用体系的使用详解》:本文主要介绍Java资源管理和引用体系的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Java的引用体系1、强引用 (Strong Reference)2、软引用 (Soft Reference)3、弱引用 (W

浅谈Redis Key 命名规范文档

《浅谈RedisKey命名规范文档》本文介绍了Redis键名命名规范,包括命名格式、具体规范、数据类型扩展命名、时间敏感型键名、规范总结以及实际应用示例,感兴趣的可以了解一下... 目录1. 命名格式格式模板:示例:2. 具体规范2.1 小写命名2.2 使用冒号分隔层级2.3 标识符命名3. 数据类型扩展命

双系统电脑中把Ubuntu装进外接移动固态硬盘的全过程

《双系统电脑中把Ubuntu装进外接移动固态硬盘的全过程》:本文主要介绍如何在Windows11系统中使用VMware17创建虚拟机,并在虚拟机中安装Ubuntu22.04桌面版或Ubunt... 目录一、首先win11中安装vmware17二、磁盘分区三、保存四、使用虚拟机进行系统安装五、遇见的错误和解决

使用FileChannel实现文件的复制和移动方式

《使用FileChannel实现文件的复制和移动方式》:本文主要介绍使用FileChannel实现文件的复制和移动方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录使用 FileChannel 实现文件复制代码解释使用 FileChannel 实现文件移动代码解释

Spring 中的循环引用问题解决方法

《Spring中的循环引用问题解决方法》:本文主要介绍Spring中的循环引用问题解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录什么是循环引用?循环依赖三级缓存解决循环依赖二级缓存三级缓存本章来聊聊Spring 中的循环引用问题该如何解决。这里聊

Node.js 数据库 CRUD 项目示例详解(完美解决方案)

《Node.js数据库CRUD项目示例详解(完美解决方案)》:本文主要介绍Node.js数据库CRUD项目示例详解(完美解决方案),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考... 目录项目结构1. 初始化项目2. 配置数据库连接 (config/db.js)3. 创建模型 (models/

浅谈配置MMCV环境,解决报错,版本不匹配问题

《浅谈配置MMCV环境,解决报错,版本不匹配问题》:本文主要介绍浅谈配置MMCV环境,解决报错,版本不匹配问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录配置MMCV环境,解决报错,版本不匹配错误示例正确示例总结配置MMCV环境,解决报错,版本不匹配在col

浅谈mysql的sql_mode可能会限制你的查询

《浅谈mysql的sql_mode可能会限制你的查询》本文主要介绍了浅谈mysql的sql_mode可能会限制你的查询,这个问题主要说明的是,我们写的sql查询语句违背了聚合函数groupby的规则... 目录场景:问题描述原因分析:解决方案:第一种:修改后,只有当前生效,若是mysql服务重启,就会失效;

电脑提示找不到openal32.dll文件怎么办? openal32.dll丢失完美修复方法

《电脑提示找不到openal32.dll文件怎么办?openal32.dll丢失完美修复方法》openal32.dll是一种重要的系统文件,当它丢失时,会给我们的电脑带来很大的困扰,很多人都曾经遇到... 在使用电脑过程中,我们常常会遇到一些.dll文件丢失的问题,而openal32.dll的丢失是其中比较

使用DrissionPage控制360浏览器的完美解决方案

《使用DrissionPage控制360浏览器的完美解决方案》在网页自动化领域,经常遇到需要保持登录状态、保留Cookie等场景,今天要分享的方案可以完美解决这个问题:使用DrissionPage直接... 目录完整代码引言为什么要使用已有用户数据?核心代码实现1. 导入必要模块2. 关键配置(重点!)3.