【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://xiahunao.cn/news/2776274.html

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

相关文章

【网络技术】【Kali Linux】Nmap嗅探(二)多设备扫描

上期实验博文&#xff1a;&#xff08;一&#xff09;简单扫描 一、实验环境 本次实验进行Nmap多设备扫描&#xff0c;实验使用 Kali Linux 虚拟机&#xff08;扫描端&#xff09;、Ubuntu 22.04虚拟机&#xff08;被扫描端1&#xff09;、Ubuntu 18.04虚拟机&#xff08;被扫…

C++ 动态规划 记忆化搜索 滑雪

给定一个 R 行 C 列的矩阵&#xff0c;表示一个矩形网格滑雪场。 矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。 一个人从滑雪场中的某个区域内出发&#xff0c;每次可以向上下左右任意一个方向滑动一个单位距离。 当然&#xff0c;一个人能够滑动到某相…

C语言----内存函数

内存函数主要用于动态分配和管理内存&#xff0c;它直接从指针的方位上进行操作&#xff0c;可以实现字节单位的操作。 其包含的头文件都是&#xff1a;string.h memcpy copy block of memory的缩写----拷贝内存块 格式&#xff1a; void *memcpy(void *dest, const void …

【buuctf--被偷走的文件】

将 ftp 流量过滤下来&#xff0c;追踪 ftp 流量&#xff0c;得到下图 先解释一下这四行什么意思&#xff1a; PASV&#xff1a; 这是FTP的命令&#xff0c;用于告知服务器在数据连接中使用被动模式&#xff08;Passive Mode&#xff09;。在被动模式下&#xff0c;数据连接的…

零基础学编程怎么入手,中文编程工具构件箱之渐变背景构件用法教程,系统化的编程视频教程上线

零基础学编程怎么入手&#xff0c;中文编程工具构件箱之渐变背景构件用法教程&#xff0c;系统化的编程视频教程上线 一、前言 今天给大家分享的中文编程开发语言工具资料如下&#xff1a; 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例…

【Linux系统学习】 4.Linux实用操作 上

Linux实用操作 1.各类小技巧&#xff08;快捷键&#xff09; 1.1 ctrl c 强制停止 Linux某些程序的运行&#xff0c;如果想要强制停止它&#xff0c;可以使用快捷键ctrl c 命令输入错误&#xff0c;也可以通过快捷键ctrl c&#xff0c;退出当前输入&#xff0c;重新输入 1…

[Java][算法 哈希]Day 01---LeetCode 热题 100---01~03

LeetCode 热题 100---01~03 ------->哈希 第一题 两数之和 思路 最直接的理解就是 找出两个数的和等于目标数 这两个数可以相同 但是不能是同一个数字&#xff08;从数组上理解就是内存上不是同一位置&#xff09; 解法一&#xff1a;暴力法 暴力解万物 按照需求 …

springboot170图书电子商务网站的设计与实现

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

带你学【自动控制原理】(二)-->第一章:分类、系统性能的基本要求、研究内容

声明:本人大学《自动控制原理》课程为全专业唯一一个满分!!!考研专业课分数145分(某985专业课),对于自控方面的知识掌握较为全面。当然,本人水平毕竟有限,博客可能存在部分错误的地方,请广大读者谅解并向本人反馈错误。   本专栏博客参考书籍为卢京潮老师的《自动控制…

java基础(2) 面向对象编程-java核心类

面向对象 面向对象对应的就是面向过程&#xff0c; 面向过程就是一步一步去操作&#xff0c;你需要知道每一步的步骤。 面向对象的编程以对象为核心&#xff0c;通过定义类描述实体及其行为&#xff0c;并且支持继承、封装和多态等特性 面向对象基础 面向对象编程&#xff0…

python多线程连接MySQL查数案例

该博文展示地是基本示例&#xff0c;实际使用时可能需要进行调整。例如&#xff0c;你可能需要添加错误处理来确保数据库连接问题不会导致脚本崩溃&#xff0c;或者你可能需要调整查询以匹配你的数据。 此外&#xff0c;你需要确保你的系统有足够的内存和处理能力来支持并行处理…

Huggingface上传模型

Huggingface上传自己的模型 参考 https://juejin.cn/post/7081452948550746148https://huggingface.co/blog/password-git-deprecationAdding your model to the Hugging Face Hub&#xff0c; huggingface.co/docs/hub/ad…Welcome&#xff0c;huggingface.co/welcome三句指…

# Memory Analyzer (MAT) 在实际开发中的使用

Memory Analyzer (MAT) 在实际开发中的使用 文章目录 Memory Analyzer (MAT) 在实际开发中的使用概述注意点基本使用检查概述获取直方图View the Dominator Tree到GC根的路径 使用示例制作堆dumpHeapDumpOnOutOfMemoryErrorJmap 生成堆Dump Mat打开堆快照HistogramThread Overv…

LLM大语言模型(六):RAG模式下基于PostgreSQL pgvector插件实现vector向量相似性检索

目录 HightLightMac上安装PostgreSQLDBever图形界面管理端创建DB 使用向量检索vector相似度计算近似近邻索引HNSW近似近邻索引示例 HightLight 使用PostgreSQL来存储和检索vector&#xff0c;在数据规模非庞大的情况下&#xff0c;简单高效。 可以和在线业务共用一套DB&#…

响应式编程详解(持续更新)

响应式编程 1.多维度看全景1.1响应式编程(Reactive Programming )1.2函数式编程&#xff08;Functional Programming, 简称FP&#xff09;1.3技术演进1.4Rx是什么1.5[响应式宣言](https://www.reactivemanifesto.org/zh-CN) 2.钻进去看本质2.1名称解释(rajava)2.2观察者模式2.3…

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

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

第一章 整车EE架构和软件发展情况

第一章 整车EE架构的基本介绍和发展 1. 架构形态 整车架构形态目前呈三种阶段&#xff1a;分布式阶段、域内集中阶段、中央计算阶段 分布式阶段 域内集中阶段 中央计算阶段 2. 架构特点 分布式阶段 各个ECU是完全分离的&#xff0c;且算力较低&#xff0c;只能实现较为简单…

一、OpenAI API介绍

Open AI API可以应用到任何的业务场景。 文本生成 创造助理 嵌入数据 语音转化 图片生成 图片输入 1. 核心概念 1.1 Text generation models OpenAI 的文本生成模型(通常被称为generative pre-trained transformers 模型简称&#xff1a;GPT),有GPT-4和G…

安全基础~通用漏洞4

文章目录 知识补充XSS跨站脚本**原理****攻击类型**XSS-后台植入Cookie&表单劫持XSS-Flash钓鱼配合MSF捆绑上线ctfshow XSS靶场练习 知识补充 SQL注入小迪讲解 文件上传小迪讲解 文件上传中间件解析 XSS跨站脚本 xss平台&#xff1a; https://xss.pt/ 原理 恶意攻击者…

Qlik Sense : where exists

什么是Exists函数 Exists() 用于确定是否已经将特定字段值加载到数据加载脚本中的字段。此函数用于返回 TRUE 或 FALSE&#xff0c;这样它可以用于 LOAD 语句或 IF 语句中的 where 子句。 信息注释您也可使用 Not Exists() 来确定是否尚未加载字段值&#xff0c;但是如果要在…