【C++11】右值引用 | 移动构造赋值 | 万能引用 | 完美转发

文章目录

  • 一、引言
  • 二、左值和右值
      • 什么是左值
      • 什么是右值
  • 三、左值引用和右值引用
      • 左值引用
      • 右值引用
      • 左值引用与右值引用的比较
  • 四、右值引用的使用场景和意义
      • 左值引用的使用场景
      • 左值引用的短板
      • 用右值引用和移动语义解决上述问题
          • 移动构造
          • 移动赋值
      • 右值引用引用左值 - std::move()
      • STL容器的接口函数更新了右值引用的版本
  • 五、完美转发
      • 模板中的“&&”是万能引用
      • std::forward()实现完美转发
      • 完美转发的使用场景


一、引言

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始,为了与右值引用(rvalue reference)区分开来,我们可以称之为左值引用 (lvalue reference)无论左值引用还是右值引用,都是给对象取别名



二、左值和右值

在了解右值引用之前,有必要先区分左值和右值。C++的表达式要么是右值( rvalue ,读作“ are-value ”),要么就是左值( lvalue ,读作 "ell-value ” )。

这两个名词是从 C 语言继承过来的,原本是为了帮助记忆:
左值可以位于赋值语句的左侧,右值则不能。

什么是左值

左值是一个表示数据的表达式(如变量名或解引用的指针/迭代器)

  • 我们可以获取左值的地址且可以对左值赋值。
  • 左值可以出现赋值符号的左边,而右值不能出现在赋值符号左边。
  • 定义被const修饰符修饰的左值,不能给它赋值,但是可以取它的地址。
  • 在需要右值的地方可以用左值来代替,但是不能把右值当成左值使用。
  • 当一个左值被当成右值使用时,实际使用的是它的内容(值)。

什么是右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)

  • 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边。
  • 右值不能取地址,左值一定可以取地址。(能否取地址是区分左右值的方式


三、左值引用和右值引用

左值引用

左值引用就是给左值的引用,给左值取别名。

int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}

右值引用

右值引用就是对右值的引用,给右值取别名。

int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 特例:可以将一个const左值引用绑定到一个右值上const int& cref = 10;// 下面三个编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}

左值引用与右值引用的比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;  // 正确,ra为a的别名
    int& ra2 = 10; // 编译失败,因为10是右值
    
  2. 但是const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10; // 正确,ra3引用了右值10
    const int& ra4 = a;  // 正确,ra4引用了左值a
    

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10; // 正确,10绑定到一个右值引用int a = 10;
    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    int&& r2 = a; // 错误,无法将左值a绑定到右值引用r2
    
  2. 但是右值引用可以引用 std::move() 后的左值。
    int&& r3 = std::move(a); //正确,右值引用可以引用move以后的左值
    


四、右值引用的使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值:

const int& cref = 10;

那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!下面是我们模拟实现的 std::string 类:

#pragma once
#include <cstring>
#include <cassert>
#include <iostream>
namespace chen
{class string{public:// 迭代器typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}// 默认构造string(const char* str = ""):_size(strlen(str)), _capacity(_size){//std::cout << "string(char* str)" << std::endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){std::cout << "string(const string& s) -- 深拷贝" << std::endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;string tmp(s);swap(tmp);return *this;}// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){std::cout << "string(string&& s) -- 移动语义" << std::endl;swap(s);}// 移动赋值string& operator=(string&& s){std::cout << "string& operator=(string&& s) -- 移动语义" << std::endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

左值引用的使用场景

左值引用做参数和做返回值都可以提高效率,减少了深拷贝:

void func1(chen::string s)
{}void func2(const chen::string& s)
{}int main()
{chen::string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}

因为我们模拟实现是string类的拷贝构造函数当中打印了提示语句,因此运行代码后通过程序运行结果就知道,值传参时调用了string的拷贝构造函数:请添加图片描述

因为string的+=运算符重载函数是左值引用返回的,因此在返回+=后的对象时不会调用拷贝构造函数,但如果将+=运算符重载函数改为传值返回,那么重新运行代码后你就会发现多了一次拷贝构造函数的调用。

我们都知道string的拷贝是深拷贝,深拷贝的代价是比较高的,我们应该尽量避免不必要的深拷贝操作,因此这里左值引用起到的作用还是很明显的。

左值引用的短板

是当函数返回对象是一个函数作用域内的局部变量,它出了函数作用域就会被销毁,就不能使用左值引用返回,只能传值返回。
例如:bit::string to_string(int value) 函数中可以看到,这里只能使用传值返回:

namespace chen
{chen::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}chen::string str;while (value > 0){int x = value % 10;value /= 10;str += (x + '0');}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}int main()
{chen::string s = chen::to_string(1234);return 0;
}

传值返回会导致至少1次拷贝构造:
请添加图片描述

如果是一些旧一点的编译器可能是两次拷贝构造:
请添加图片描述

对于vs2022,有可能只调用一次构造函数,没错,是构造函数,极致的优化:
请添加图片描述

因为即使编译器支持 C++11,也不能确保一定会调用移动构造函数。具体调用的是拷贝构造函数还是移动构造函数,取决于编译器对于返回对象优化的实现和对移动语义的判断。

用右值引用和移动语义解决上述问题

移动构造

string中增加移动构造移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是“窃取”别人的资源来构造自己

// 移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{std::cout << "string(string&& s) -- 移动语义" << std::endl;swap(s);
}

可以这样鼓励编译器使用移动构造函数:

// chen::string s1 = chen::to_string(1234); // 由于编译器的优化,这里调用的可能是构造函数
chen::string s2 = std::move(chen::to_string(1234)); // 显式move一下来通知编译器调用移动构造,来构造s2//运行结果:
// string(char* str) -- 构造函数
// string(string&& s) -- 移动语义
移动赋值

string类中增加移动赋值函数,再去调用to_string(1234),不过这次是将
to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string& operator=(string&& s)
{std::cout << "string& operator=(string&& s) -- 移动语义" << std::endl;swap(s);return *this;
}
chen::string s;
s = chen::to_string(1234);//运行结果:
// string(char* str) -- 构造函数
// string(char* str) -- 构造函数
// string& operator=(string&& s) -- 移动语义

右值引用引用左值 - std::move()

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过std::move()函数将左值转化为右值。

C++11中,std::move 的定义位于头文件 <utility> 中,其定义如下:

template <class _Ty>
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept 
{return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

几点说明:

  • 这里的 remove_reference_t 是一个辅助模板,用于去除传入类型的引用。move 函数接受一个通用引用 T&&(即右值引用),并返回一个右值引用 T&&
  • move函数中_Arg参数的类型不是右值引用,而是万能引用。万能引用跟右值引用的形式一样,但是右值引用需要是确定的类型。
  • 一个左值被move以后,它的资源可能就被转移给别人了,因此要避免使用一个被move后的左值
  • move函数的名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

STL容器的接口函数更新了右值引用的版本

请添加图片描述


请添加图片描述

请添加图片描述

如果list容器当中存储的是string对象,那么在调用push_back向list容器中插入元素时,可能会有如下几种插入方式:

void push_back (value_type&& val);
int main()
{list<chen::string> lt;bit::string s1("1111");// 这里调用的是拷贝构造lt.push_back(s1);// 下面调用都是移动构造lt.push_back("2222");lt.push_back(std::move(s1));return 0;
}
//运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

请添加图片描述



五、完美转发

模板中的“&&”是万能引用

模板中的&&不代表右值引用,而是 万能引用 ,其既能接收左值又能接收右值。下面是的T是一个万能引用:

template<class T> 
void PerfectForward(T&& t) 
{//... 
}

右值引用和万能引用的区别是:

  • 右值引用需要是确定的类型,而万能引用是根据传入实参的类型进行推导,如果传入的实参是一个左值,那么这里的形参t就是左值引用,如果传入的实参是一个右值,那么这里的形参t就是右值引用。
  • 换句话说:右值引用的类型在声明时就已经确定,而通用引用的类型是根据传入的实参类型进行推导的:
    int&& rvalue_ref = 42;  // 右值引用,类型是 int&&
    

万能引用因此更加灵活,可以接受各种值类别的参数。

下面重载了四个Func函数,这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。如下:

void Func(int& x)
{cout << "左值引用" << endl;
}
void Func(const int& x)
{cout << "const 左值引用" << endl;
}
void Func(int&& x)
{cout << "右值引用" << endl;
}
void Func(const int&& x)
{cout << "const 右值引用" << endl;
}template<class T>
void PerfectForward(T&& t)
{Func(t);
}int main()
{int a = 10;PerfectForward(a);       //左值PerfectForward(move(a)); //右值const int b = 20;PerfectForward(b);       //const 左值PerfectForward(move(b)); //const 右值return 0;
}

但实际调用PerfectForward()函数时传入左值和右值,最终都匹配到了左值引用版本的Func()函数,调用PerfectForward()函数时传入const左值const右值,最终都匹配到了const左值引用版本的Func函数,如下:
请添加图片描述

根本原因就是,编译器选择调用左值引用版本的Func(),通常希望对传递的对象进行修改,而将右值引用看作左值引用可以确保安全的修改,所以在PerfectForward函数中调用Func()函数时会将t识别成左值。

[!Quote] 举个简单的例子:

#include \<iostream>void Func(int&& x) 
{// 在函数内部,x 被当作左值引用x += 10;std::cout << "Inside Func: " << x << std::endl;
}int main() 
{int a = 5;// 将右值引用传递给函数Func(std::move(a));// 在这里,a 可能被移动了,但在函数外部,a 仍然是左值std::cout << "Outside Func: " << a << std::endl;return 0;
}

结果:请添加图片描述

在这个例子中,std::move(a) 将左值 a 转换为右值引用,并传递给 Func 函数。在函数内部,x 被当作左值引用,但我们仍然可以对它进行修改。在函数外部,a 仍然是左值,但在传递给函数时可能已经发生了移动。

总结:右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发

std::forward()实现完美转发

想要在传参的过程中保留对象原生类型属性,可以使用std::forward(),比如:

template<class T>
void PerfectForward(T&& t)
{Func(std::forward<T>(t));
}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数:
请添加图片描述

完美转发的使用场景

下面模拟实现了一个简化版的list类,类当中分别提供了左值引用版本和右值引用版本的push_backinsert函数。

namespace chen
{template<class T>struct ListNode{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;};template<class T>class List{typedef ListNode<T> Node;public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的pushbackvoid PushBack(const T& x){Insert(_head, x);}//右值引用版本的pushbackvoid PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x)); // 关键位置1}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x)); // 关键位置2}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置3// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, const T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置4// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;};
}

下面定义一个list对象,list容器中存储的就是之前模拟实现的string类,这里分别传入左值和右值来调用不同版本的push_back。

int main()
{chen::List<chen::string> lt;chen::string s("1111");lt.PushBack(s);      // 调用左值引用版本的push_backlt.PushBack("2222"); // 调用右值引用版本的push_backreturn 0;
}

请添加图片描述

只要想保持右值的属性,在每次右值传参时都需要用std::forward进行完美转发,实际STL库中也是通过完美转发来保持右值属性的。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习系列——(十七)聚类

引言 在当今数据驱动的时代&#xff0c;机器学习已经成为了解锁数据潜能的关键技术之一。其中&#xff0c;聚类作为机器学习领域的一个重要分支&#xff0c;广泛应用于数据挖掘、模式识别、图像分析等多个领域。本文旨在深入探讨聚类技术的原理、类型及其应用&#xff0c;为读…

《MySQL 简易速速上手小册》第8章:事务管理和锁定策略(2024 最新版)

文章目录 8.1 理解 MySQL 中的事务8.1.1 基础知识8.1.2 重点案例&#xff1a;使用 Python 实现银行转账事务8.1.3 拓展案例 1&#xff1a;处理并发事务8.1.4 拓展案例 2&#xff1a;使用 Python 监控事务状态 8.2 锁定机制和事务隔离级别8.2.1 基础知识讲解8.2.2 重点案例&…

迷你世界勒索病毒,你的文件被删了吗?

前言 笔者在某恶意软件沙箱平台分析样本的时候&#xff0c;发现了一款比较有意思的勒索病毒MiniWorld迷你世界勒索病毒&#xff0c;它的解密界面与此前的WannaCry勒索病毒的界面相似&#xff0c;应该是作者仿冒的WannaCry的UI&#xff0c;如下所示&#xff1a; 这款勒索病毒既…

【OrangePi Zero2的系统移植】交叉编译工具链配置、wiringOP库、智能分类工程代码

一、交叉编译工具链配置 二、交叉编译wiringOP库 三、交叉编译智能分类工程代码 四、Makefile 用于编译 WiringPi 库 一、交叉编译工具链配置 1、关于编译 编译是指将源代码文件&#xff08;如C/C文件&#xff09;经过预处理、编译、汇编和链接等步骤&#xff0c;转换为可执…

发廊理发店微信小程序展示下单前端静态模板源码

模板描述&#xff1a;剪发小程序前端源码&#xff0c;一共五个页面&#xff0c;包括店铺、理发师、订单、我的等页面 注&#xff1a;该源码是前端静态模板源码&#xff0c;没有后台和API接口

如何用Hexo搭建一个优雅的博客

引言 在数字化时代&#xff0c;拥有一个个人博客已经成为许多人展示自己技能、分享知识和与世界互动的重要方式。而在众多博客平台中&#xff0c;Hexo因其简洁、高效和易于定制的特点而备受青睐。本文将详细介绍如何从零开始搭建一个Hexo博客&#xff0c;让你的个人博客在互联…

Lombok 高级说明

优质博文&#xff1a;IT-BLOG-CN 一、痛点 【1】代码臃肿&#xff1a;POJO中的getter/setter/equals/hashcode/toString等&#xff1b; 【2】样板式代码&#xff1a;I/O流的关闭操作等&#xff1b; Lombok是一个可以通过注解简化Java代码开发的工具&#xff0c;能够在我们编…

虚继承 -- 解决菱形继承问题以及无法跨继承访问

目录 什么是菱形继承? 菱形继承造成的问题: 如何解决这种问题: 代码分析: 注意: 什么是菱形继承? 上图就是一个菱形继承的例子。 菱形继承: 有两个类(Father,Mother)&#xff0c;都继承于一个类(Human),然后还有一个类&#xff0c;又同时继承于这两个类(Son)…

CentOS 7安装Nodejs

说明&#xff1a;本文介绍如何在云服务器上CentOS 7操作系统上安装Nodejs。以及安装过程中遇到的问题。 下载压缩包&解压 首先&#xff0c;先去官网下载Linux版本的Node。 将下载下来的压缩包&#xff0c;上传到云服务器上&#xff0c;解压。配置环境变量。 &#xff08…

保护我方水晶,2024 数据库安全工具盘点

在数据价值堪比石油的数字时代&#xff0c;对每个组织而言&#xff0c;保护这一核心资产显得尤为重要。无论是来自外部的黑客攻击和恶意软件&#xff0c;还是源于内部的人为失误和内鬼行为&#xff0c;威胁无处不在。本文将介绍几款先进的数据库安全工具&#xff0c;从不同维度…

统一数据格式返回,统一异常处理

目录 1.统一数据格式返回 2.统一异常处理 3.接口返回String类型问题 1.统一数据格式返回 添加ControllerAdvice注解实现ResponseBodyAdvice接口重写supports方法&#xff0c;beforeBodyWrite方法 /*** 统一数据格式返回的保底类 对于一些非对象的数据的再统一 即非对象的封…

FastDFS安装并整合Openresty

FastDFS安装 一、环境--centos7二、FastDFS--tracker安装2.1.下载2.2.FastDFS安装环境2.3.安装FastDFS依赖libevent库2.4.安装libfastcommon2.5.安装 libserverframe 网络框架2.6.tracker编译安装2.7.文件安装位置介绍2.8.错误处理2.9.配置FastDFS跟踪器(Tracker)2.10.启动2.11…