右值引用以及move移动语义和forward 完美转发

news2024/11/21 2:38:07

右值引用

右值引用最简单的作用:可以避免无谓的复制,提高了程序性能(在移动构造函数中有体现)。

什么是右值

最基本的解释:

左值可以取地址、位于等号左边;
右值没法取地址,位于等号右边。(或者函数的返回值等)

例如:

struct A {
    A(int a = 0) {
        a_ = a;
    }
    int a_;
};
A a = A();
  • 其中a可以通过 & 取地址,位于等号左边,所以a是左值。
  • A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

左值引用和右值引用

引用:引用本质是别名,可以通过引用来修改变量的值,传参时传引用可以避免拷贝。

左值引用

左值引用:能指向左值,不能指向右值的就是左值引用

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

代码中第三行,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是有特例const。

const左值引用

const int &ref_a = 5; // 编译通过

const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const & 作为函数参数的原因之一,这可以按照固定搭配记住。例如:

void push_back (const value_type& val);
...
vec.push_back(5);

如果函数参数没有const , vec.push_back(5) 这样的代码就无法编译通过。

右值引用

右值引用:右值引用专可以指向右值,不能指向左值

右值引用的标志是&& :

int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值

std::move函数

右值引用可以使用std::move可以指向左值

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    int a = 5; // a是个左值
    int &ref_a_left = a; // 左值引用指向左值
    int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
    cout << ref_a_right << endl; // 打印结果:5
    cout << a; // 打印结果:还是5
    return 0;
}
//代码编译时使用C++11新特性
//g++ main.cpp -o main -std=c++11

std::move把一个变量a里的内容移动到另一个变量ref_a_right了吗?
不是!在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。

std::move函数:

  • std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。
  • 其实现等同于一个类型转换:static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。

右值引用的含义

右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6;
//等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 此时temp也等于6
//两个变量的地址是相同的;&temp,&ref_a是一样的

左值引用、右值引用的本身

被声明出来的左、右值引用都是左值。

因为被声明出的左右值引用是有地址的,都是左值。如下:

// 函数的形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}

如上代码所示int &&ref_a_right = std::move(a); 是一个右值引用,但是ref_a_right是一个左值。

结论:

从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

右值引用避免深拷贝

深拷贝可以避免重复析构的问题,可参考帖子深拷贝与浅拷贝定义以及案例说明。但是深拷贝也带来了性能的消耗,这里可以通过移动构造函数避免额外的内存消耗。

#include <iostream>
using namespace std;
class A
        {
        public:
            A() :m_ptr(new int(0)) {
                cout << "constructor A" << endl;
            }
            A(const A& a) :m_ptr(new int(*a.m_ptr)) {
                cout << "copy constructor A" << endl;
            }
            // 移动构造函数,可以浅拷贝
            A(A&& a) :m_ptr(a.m_ptr) {
                a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
                cout << "move constructor A" << endl;
            }
            ~A(){
                cout << "destructor A, m_ptr:" << m_ptr << endl;
                if(m_ptr)
                    delete m_ptr;
            }
        private:
            int* m_ptr;
        };
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
int main()
{
    {
        A a = Get(false); // 正确运行
    }
    cout << "main finish" << endl;
    return 0;
}

运行结果:
在这里插入图片描述

建议对比深拷贝与浅拷贝定义以及案例说明中的代码。

  • 可以看到A Get(bool flag),返回参数为A对象,函数中我们返回的a,b是临时变量,所以这里自动会使用移动构造函数通过右值引用接收临时变量。
  • 上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。
  • 从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。
  • 这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
  • 移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。

结论:
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高
C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

(move)移动语义

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持(也就是需要移动构造函数)。

// 用c++11的右值引用来定义这两个函数
//MyString类的移动构造函数
MyString(MyString&& str) {
	std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
	m_len = str.m_len;
	m_data = str.m_data; //避免了不必要的拷贝
	str.m_len = 0;
	str.m_data = NULL;
}
...
MyString c = std::move(a); // Move Constructor is called! 将左值转为右值

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

forward 完美转发

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

如下面这个简单的例子:

int &&a = 10;
int &&b = a; //错误

这里a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。
这里就可以使用std::forward()完美转发,就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward<int>(a);

这样就是正确的,这里有点像move移动语义的意思,forward是有把左值转为右值的功能,但尽量要以完美转发的角度去理解forward。

上面的是一个简单的完美转发的例子,但完美转发最常见还是处理在模板类中会存在一种“属性变换”现象。

#include <iostream>
using namespace std;
template <class T>
void Print(T &t)
{
    cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
    cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
    Print(t);
    Print(std::move(t));
    Print(std::forward<T>(t));
}
int main()
{
    cout << "-- func(1)" << endl;
    func(1);
    int x = 10;
    int y = 20;
    cout << "-- func(x)" << endl;
    func(x); // x本身是左值
    cout << "-- func(std::forward<int>(y))" << endl;
    func(std::forward<int>(y)); //这里将输入参数转为右值传入了
    return 0;
}

运行结果:
在这里插入图片描述

解释一下func(1)结果 :

  1. 由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在func()函数体内部,调用Print(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具名的变量,所以它是一个左值),因此,示例测试结果第一个Print被调用,打印出“L1";
  2. 调用Print(std::move(v))是将v变成一个右值(v本身也是右值),因此输出”R1";
  3. 调用Print(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void Print(T&&t)函数打印“R1”.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UnityVR--组件9--VideoPlayerAudioSource

目录 前言 视频组件VideoPlayer参数解释 RenderMode渲染方式 VideoPlayer类中的API 音频组件AudioSource参数解释 AudioSource类中的常见API&简单应用 前言 在之前的VR场景中已经使用过VideoPlayer播放视频&#xff08;Unity.UI的交互&#xff08;6&#xff09;-播放…

chatgpt赋能python:Python怎么快速读取一组图片的RGB值?

Python怎么快速读取一组图片的RGB值&#xff1f; 简介 Python是一种非常流行的程序设计语言&#xff0c;它具有易于学习、简洁明了的语法和强大的功能。Python被广泛应用于数据分析、人工智能、科学计算、Web开发、游戏开发等领域。在这篇文章中&#xff0c;我们将介绍如何使…

chatgpt赋能python:Python快速缩进技巧与优化提升

Python快速缩进技巧与优化提升 介绍 在Python中&#xff0c;缩进是代码块的唯一标识符。这种缩进机制使得Python代码看起来更加清晰和易于阅读。同时&#xff0c;正确的缩进也是Python程序能否正常运行的重要因素。然而&#xff0c;大量的缩进可能会导致程序员的效率降低&…

Android系统Handler详解

目录 一&#xff0c;背景介绍 1.1 简介 1.2 核心概念 1.3 Handler 背后的生产者-消费者模型 二&#xff0c;Handler机制原理 2.1 消息模型 2.2 Handler原理概述 2.3 Handler与管道通信 三&#xff0c;实战 3.1 创建 Handler 3.2 子线程向主线程 3.3 主线程向子线程…

C/C++爱心代码“你把握不住的,让哥来~”祝你找到另一半

目录 第一种心形 加点好玩的 最后一忠心形&#xff08;会变色的爱心&#xff09; 618多得图书活动来啦 第一种心形 这次需要用到头文件#include<windows.h> #include<stdio.h> #include<windows.h> 以下是完整代码 #include<stdio.h> #include<…

chatgpt赋能python:Python中如何使用Math库进行数学计算

Python中如何使用Math库进行数学计算 Python是一种功能强大的编程语言&#xff0c;但对于许多数字计算、三角函数和其他复杂的数学问题&#xff0c;Python本身并不提供内置支持。为了解决这些问题&#xff0c;Python提供了一个名为Math的库。本文将介绍如何引入Math库&#xf…

【王道·操作系统】第三章 内存管理【未完】

一、内存管理 1.1 内存的基础知识 内存可存放数据&#xff0c;程序执行前需要先放到内存中才能被CPU处理——缓和CPU与硬盘之间的速度矛盾内存地址从0开始&#xff0c;每个地址对应一个存储单元 按字节编址&#xff1a;每个存储单元大小为1字节(B)&#xff0c;即8个二进制位按…

OJ Prime Gap

目录 1.题目 2.中文翻译 3.题意 4.代码 5.知识点 range的倒序处理&#xff1a; 1.题目 Prime Gap Description The sequence of n − 1 consecutive composite numbers (positive integers that are not prime and not equal to 1) lying between two successive prime…

软考A计划-2023系统架构师-知识点集锦(3/4)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

DAY20:二叉树(十)最大二叉树+合并二叉树

文章目录 654.最大二叉树思路遍历顺序 完整版变量作用域的问题 修改后的完整版递归进一步理解关于终止条件 优化时间复杂度和空间复杂度的优化补充&#xff1a;二叉树的高度logn 617.合并二叉树思路完整版定义新二叉树的写法 654.最大二叉树 本题做的时候也卡了一些问题&#…

Gitlab CI/CD概述

前言 CI/CD 是一种持续开发软件的方法&#xff0c;可以不断的进行构建、测试和部署代码迭代更改。这种迭代有助于减少基于错误或失败的版本进行开发新代码的可能性。使用这种方法&#xff0c;从新代码开发到部署&#xff0c;可以减少人工干预甚至不用干预。 达到持续的方法主要…

Python实例属性和实例方法_类对象、类属性、类方法、静态方法

一、实例属性 实例属性是从属于实例对象的属性&#xff0c;也称为“实例变量”。他的使用有如下几个要点&#xff1a; 实例属性一般在__init__()方法中通过如下代码定义&#xff1a; self.实例属性名 初始值 在本类的其他实例方法中&#xff0c;也是通过self进行访问&#x…

QGIS下载天地图瓦片数据

说明 介绍利用QGIS下载天地图瓦片数据。 关键字:window、QGIS、天地图 环境准备 QGIS版本为3.28.3 基本步骤 一、在Browser面板中找到XYZ Tiles 二、New Connection 参数设置 天地图参数 name:如"天地图影像"url :https://t5.tianditu.gov.cn/DataServer?T=…

U-Mail邮件系统:严防数据泄露 保障企业数据安全

在数字经济时代&#xff0c;数据已经成为企业的核心生产要素&#xff0c;也由此滋生了牟取暴利的黑色产业链&#xff0c;企业数据泄露事件有增无减。根据IdentifyTheft Research Center中心的数据显示&#xff0c;2022年世界范围内的数据泄露事件比2021年增长了14%。其中&#…

chatgpt赋能python:Python如何得出结果:从基础语法到高级算法

Python如何得出结果&#xff1a;从基础语法到高级算法 作为一种流行的编程语言&#xff0c;Python被广泛应用于数据分析、人工智能、Web开发等领域。但是&#xff0c;Python也是一种非常值得学习的SEO工具&#xff0c;它可以帮助你得出有关网站排名、竞争对手分析、关键词选择…

JavaScript-Vue

2 Vue 2.1 Vue概述 通过我们学习的htmlcssjs已经能够开发美观的页面了&#xff0c;但是开发的效率还有待提高&#xff0c;那么如何提高呢&#xff1f;我们先来分析下页面的组成。一个完整的html页面包括了视图和数据&#xff0c;数据是通过请求 从后台获取的&#xff0c;那么…

游览器获取用户位置信息,不同游览器获取位置信息不一致

问题 游览器获取用户位置信息&#xff0c;不同游览器获取位置信息不一致 详细问题 对于下述代码 <!DOCTYPE html> <html> <head><title>获取用户经纬度</title><script>function getLocation() {if (navigator.geolocation) {navigato…

数据库原理

做应用开发的同学常常觉得数据库由DBA运维&#xff0c;自己会写SQL就可以了&#xff0c;数据库原理不需要学习。其实即使是写SQL也需要了解数据库原理&#xff0c;比如我们都知道&#xff0c;SQL的查询条件尽量包含索引字段&#xff0c;但是为什么呢&#xff1f;这样做有什么好…

第三章Java锁—基础

文章目录 乐观锁和悲观锁悲观锁悲观锁的实现方式 乐观锁乐观锁的实现方式版本号实现的大致流程 8锁案例弄清synchronized锁了什么3个体现同步方法和同步块&#xff0c;哪个是更好的选择 字节码角度分析synchronized实现文件反编译技巧synchronized同步代码块synchronized普通同…

pikachu靶场-Over Permission

Over Permission&#xff08;越权&#xff09; 用户A 的权限小于用户B 的权限&#xff0c;此时用用户 A 的权限去操作用户 B 的数据&#xff0c;如果能够操作成功&#xff0c;就称之为越权操作。 越权漏洞一般容易出现在权限页面&#xff08;需要登录的页面&#xff09;增、删…