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

相关文章

浅谈主机加固,六种有效的主机加固方法

在数字化时代,数据的价值不言而喻,但随之而来的安全威胁也日益严峻。从勒索病毒到内部泄露,企业的数据安全面临着前所未有的挑战。为了应对这些挑战,一种全新的主机加固解决方案应运而生。 MCK主机加固解决方案,采用先进的安全容器中间件技术,构建起一套内核级的纵深立体防护体系。这一体系突破了传统安全防护的局限,即使在管理员权限被恶意利用的情况下,也能确保服务器的安全稳定运行。 普适主机加固措施:

我在移动打工的日志

客户:给我搞一下录音 我:不会。不在服务范围。 客户:是不想吧 我:笑嘻嘻(气笑) 客户:小姑娘明明会,却欺负老人 我:笑嘻嘻 客户:那我交话费 我:手机号 客户:给我搞录音 我:不会。不懂。没搞过。 客户:那我交话费 我:手机号。这是电信的啊!!我这是中国移动!! 客户:我不管,我要充话费,充话费是你们的 我:可是这是移动!!中国移动!! 客户:我这是手机号 我:那又如何,这是移动!你是电信!!

用Unity2D制作一个人物,实现移动、跳起、人物静止和动起来时的动画:中(人物移动、跳起、静止动作)

上回我们学到创建一个地形和一个人物,今天我们实现一下人物实现移动和跳起,依次点击,我们准备创建一个C#文件 创建好我们点击进去,就会跳转到我们的Vision Studio,然后输入这些代码 using UnityEngine;public class Move : MonoBehaviour // 定义一个名为Move的类,继承自MonoBehaviour{private Rigidbo

理解分类器(linear)为什么可以做语义方向的指导?(解纠缠)

Attribute Manipulation(属性编辑)、disentanglement(解纠缠)常用的两种做法:线性探针和PCA_disentanglement和alignment-CSDN博客 在解纠缠的过程中,有一种非常简单的方法来引导G向某个方向进行生成,然后我们通过向不同的方向进行行走,那么就会得到这个属性上的图像。那么你利用多个方向进行生成,便得到了各种方向的图像,每个方向对应了很多

简单的角色响应鼠标而移动

actor类 //处理移动距离,核心是找到角色坐标在世界坐标的向量的投影(x,y,z),然后在世界坐标中合成,此CC是在地面行走,所以Y轴投影始终置为0; using UnityEngine; using System.Collections; public class actor : MonoBehaviour { public float speed=0.1f; CharacterCo

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

前言 PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)。现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完

JavaSE(十三)——函数式编程(Lambda表达式、方法引用、Stream流)

函数式编程 函数式编程 是 Java 8 引入的一个重要特性,它允许开发者以函数作为一等公民(first-class citizens)的方式编程,即函数可以作为参数传递给其他函数,也可以作为返回值。 这极大地提高了代码的可读性、可维护性和复用性。函数式编程的核心概念包括高阶函数、Lambda 表达式、函数式接口、流(Streams)和 Optional 类等。 函数式编程的核心是Lambda

17 通过ref代替DOM用来获取元素和组件的引用

重点 ref :官网给出的解释是: ref: 用于注册对元素或子组件的引用。引用将在父组件的$refs 对象下注册。如果在普通DOM元素上使用,则引用将是该元素;如果在子组件上使用,则引用将是组件实例: <!-- vm.$refs.p will be the DOM node --><p ref="p">hello</p><!-- vm.$refs.child will be the c

物联网之流水LED灯、正常流水灯、反复流水灯、移动流水灯

MENU 硬件电路设计软件程序设计正常流水LED灯反复流水LED灯移动流水LED灯 硬件电路设计 材料名称数量直插式LED1kΩ电阻杜邦线(跳线)若干面包板1 每一个LED的正极与开发板一个GPIO引脚相连,并串联一个电阻,负极接GND。 当然也可以选择只使用一个电阻。 软件程序设计 正常流水LED灯 因为要用到多个GPIO引脚,所以最好把所有的GPI

12C 新特性,MOVE DATAFILE 在线移动 包括system, 附带改名 NID ,cdb_data_files视图坏了

ALTER DATABASE MOVE DATAFILE  可以改名 可以move file,全部一个命令。 resue 可以重用,keep好像不生效!!! system照移动不误-------- SQL> select file_name, status, online_status from dba_data_files where tablespace_name='SYSTEM'