当前位置: 首页 > news >正文

【C++11】其他一些新特性 | 右值引用 | 完美转发

目录

前言:

一:decltyped关键字

二:新容器

三:支持初始化列表构造(initializer_list)

四:右值和右值引用 

1.什么是左值? 

2.什么是右值? 

3.右值引用

4.交叉使用 

5.move函数模板

五:观察底层 

六:右值引用的意义

1.不考虑编译器优化场景

七:移动构造

八:移动赋值 

九:右值插入方法

十:完美转发(万能引用)

总结:


前言:

我们已经掌握了C++的大部分语法,并已经能对其熟练使用,但是时代是在发展的,C++11有更新了很多新特性,其中有一些可以极大的提升效率,我们本篇来学习一下。

一:decltyped关键字

这个功能和auto类似,可以达到简化代码的作用,我们直接看代码:

//declaration 宣言
int main()
{map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();//此时我们想用一个vector存储很多map<string, string>::iterator对象//vector< map<string, string>::iterator> v;//但是你不想写就可以这样vector<decltype(it)> v;decltype(it) it1; //定义另外一个同类型的变量//decltype根据给定表达式推导其类型 获取表达式的精确类型(如返回值类型)return 0;
}

这里就是全部都是自动推导,这里给出decltype和auto的区别:

总结:

  • auto是“我想要一个变量,类型你帮我猜”。
  • decltype是“告诉我这个表达式到底是什么类型”。
  • 都可简化代码。 

二:新容器

C++11新增了几个容器,下面有框的就是新容器:

我们最常用的就是unordered系列,至于array(本质就是数组)这个容器,就是会将错误检查的更加严格,比如越界报错等。

forward底层就是单向链表,都很鸡肋,没啥用。

三:支持初始化列表构造(initializer_list)

我们知道C++11可以用 { } 来构造对象,而且可以加上=也可以不加。之前也提到过比如这样初始化一个对象:

list<int> l = { 1, 2, 3, 4, 5 };

我们看看initializer_list底层是什么?

可以看到它是一个类模板,其实没有什么神秘的,实质上就是两个指针,一个指向开始,一个指向结尾。所以我们自己可以实现初始化列表进行构造,比如我们当时实现的红黑树添加这个构造:

//写一个initializer_list的构造方法
RBTree(initializer_list<pair<K, V>> il)
{for (auto e : il){Insert(e);}
}

四:右值和右值引用 

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

1.什么是左值? 

什么是左值?什么是左值引用? 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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;
}

2.什么是右值? 

什么是右值?什么是右值引用? 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。 

观察以下代码:

int Add(int x, int y)
{return x + y;
}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);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;Add(x, y) = 3;  //和fmin一样return 0;
}

注:左值和右值我们不能通过是赋值运算符的左边和右边区分,而应该是按照能够取地址进行记忆。 

右值本质就是一些临时对象。 

以下代码是左值:

string s("11111");
s[0];    //这里是左值

因为底层返回的是引用:

char& operator[](size_t i)
{return _str[i];
}

3.右值引用

我们可以像给左值取别名一样给右值取别名,也就是右值引用

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);return 0;
}

4.交叉使用 

左值引用能不能给右值取别名?右值引用能不能给左值取别名呢?

这里先用左值引用对右值进行引用:

因为这些都是临时对象,具有常性,所以要加上const。 

因为左值先出来,所以功能比较完善。

像我们之前写的代码:

//外部
void push_back(const T& x); 既能接受左值 又能接收右值//内部:
vector<string> v;
string s1("1111");
v.push_back(s1);
v.push_back("11111");//隐式类型转换

5.move函数模板

既然我们已经可以用左值引用引用右值了,那么是否可以用右值引用引用左值呢?可以,C++11提供了一个move函数模板,它的功能就是将左值转换为右值。

五:观察底层 

我们接下来看看底层是如何对右值引用进行处理的(这里需要一些汇编基础,可以自行跳过,想学习的可以先去看这篇文章:函数的栈帧_栈 返回地址-CSDN博客 里面非常详细的讲解了最根本的命令及流程),我们调试以下代码:

int main()
{int x = 0;int& r1 = x;        //左值引用int&& rr1 = x + 10; //右值引用return 0;
}

我们接下来进入底层观察,我们之前讲到过,左值引用不开空间对应的右值引用也不开空间。我们之前讲到过左值引用在底层本质就是指针。那么接下来我们反汇编这段代码看看右值引用底层是什么逻辑(注意这里没有显示符号名)。 

这里提示如何进入反汇编:打上断点之后调试代码,右击鼠标查看反汇编。

首先建立函数栈帧,并把x的值存储在ebp-12(0Ch)的地址上:

之后执行move:

也就是将改地址的值赋给eax,至于eax在哪,我们先不关心,只需要关心它是一个寄存器即可。又执行了add也就是+操作:

之后执行mov:

之后进行了lea(加载地址)和mov(移动):

其实最终就相当于用一个指针来记录这个地址,之后可以对其操作。 

我们再来观察一下指针的汇编:

所以底层的引用本质都是指针,但我们这里只讨论语言层,但是要了解其本质。 

六:右值引用的意义

先观察代码: 

string s1("1111");
string&& rx1 = (string&&)s1; // 强制转换为右值引用

右值其实也有地址,否则我们的右值引用就无法对其操作。

我们最开始学习引用是为了有一些事代替指针,方便学习。之后知道了可以减少拷贝。但其实引用的意义就是减少拷贝!

左值引用解决的场景:引用传参/引用传返回值

但是其没有彻底解决的问题就是传返回值(一旦声明周期结束,必须使用传值返回,增加了拷贝)。

string s1 = "abcd";

这条语句调用的是构造而不是赋值重载! 

这里为了了解右值引用的功能,我们先实现一个string类(之前已经实现过了,复制局部代码即可):

namespace bit
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}~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];if (_str){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){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};
}

之后实现to_string方法(也就是将整形转换为string的方法)。

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

这是我们必须返回str这个局部对象,不能返回引用,因为出了作用域就会销毁。 

右值引用就是为了解决这样的场景。

1.不考虑编译器优化场景

现在的编译器很高级,会优化很多东西,这里我们先忽略编译器优化场景。执行以下代码:

bit::string s1 = to_string(1234);

这个正常的步骤是:to_string实例化一个str(string对象)后,在出函数作用域的时候会生成一个临时的string对象,此时会调用拷贝构造;之后该main函数中的s1又会使用拷贝构造完成自己的实例化。

在VS22下,这里优化的很多,甚至连一次拷贝构造都没有了,但是我们就讨论最根本的未优化情况。 

此时我们不能修改to_string的返回值返回右值引用,因为str是一个局部变量,出作用域就会销毁,右值引用不会延长声明周期

此时怎么办?

七:移动构造

所以提供了移动构造。

//移动构造 右值引用
//临时创建的对象 用完就要消亡
string(string&& s): _str(nullptr)
{cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);
}

在此之前我们写的拷贝构造参数为const string& s可以接收右值也可以接受左值,有了移动构造之后,只要是右值构造string就走移动构造。 

C++把右值分类为:

  • 纯右值:内置类型右值
  • 将亡值:类类型的右值(匿名对象、传值返回的临时对象)

如果不考虑优化,这里调用to_string返回str就是一次拷贝构造,拷贝构造后生成了一个临时的string对象,之后移动构造,直接抢夺临时对象资源交换即可。 

此时我们有移动构造,编译器优化后是什么样子呢?首先不产生中间临时对象,之后将str隐式move识别为右值,之后直接走移动构造。

大家是否还记得我们之前实现的杨辉三角?

我们返回的不是引用,里面的两个vector使用两次拷贝构造代价非常大,强烈不建议这样写。 

所以没有移动构造之前,我们不返回vector<vector<int>>,而是返回void,把vector<vector<int>>当做传入的参数,作为引用来修改它:

//内层调用
vector<vector<int>> ret;
generate(100, ret);//函数
void generate(int numRows, vector<vector<int>>&vv)
{//...
}

但是我们有了移动构造就无所谓了,彻底解决了这里的问题。 

所以C++11中,各种容器都提供了移动构造。

深拷贝的类,移动构造才有意义;像日期类没有写移动构造的意义。

八:移动赋值 

此时我们修改代码:

bit::string s1;
s1 = to_string(1234); 

如果我们像以上方法调用,先看运行结果:

我们像这样写代码,还是会调用赋值运算符,将临时对象深拷贝给s1。

所以就有了移动赋值。

//移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}

首先是s1的构造,之后进入to_string函数构造str,返回临时对象,之后会调用移动赋值,开销更小。 

所以最终都是为了解决传值返回的问题。

tips:Linux下可以关闭所有拷贝构造优化,命令为 -fno-elide-constructors

调试以下代码:

int main()
{list<bit::string> lt;bit::string s1("1111111");lt.push_back(s1);lt.push_back(bit::string("22222222"));return 0;
}

这里一定记得:使用const T& val接收参数没有发生任何构造! 

这里我们如果对s1进行move的话,最后走的也是移动构造。 

注意:如果此时我们move(s1)是否会改变s1的属性为右值呢?并不会,因为move是一个函数,它的返回值是右值,但并不会影响原来s1的属性 

九:右值插入方法

C++11所有的容器插入方法都提供了右值插入的方法。为了验证,我们添加之前写过的list.h到这个项目中。

所以我们将push_back和insert都提供右值插入版本:

//写一个右值的push_back
void push_back(T&& val)
{//复用insertinsert(end(), val);
}
iterator insert(iterator pos, T&& val)
{//这里是在传入的迭代器前面插入数据Node* newNode = new Node(val);Node* cur = pos._head;Node* prev = cur->_prev;newNode->_prev = prev;newNode->_next = cur;prev->_next = newNode;cur->_prev = newNode;//不要修改调用者的迭代器 而是创建一个新的迭代器返回return iterator(newNode);
}

我们调试这段代码:

可以发现,它并没有走右值的insert版本。

这里补充一个细节:右值引用本身的属性是左值!这样才能转移资源!

//其中 r1 是左值!
string&& r1 = string("12345");

所以上面的代码最终走的还是左值版本的insert。 所以我们修改右值引用的push_back,传入insert为move(val)。

void push_back(T&& val)
{//复用insertinsert(end(), move(val));
}

//移动构造
ListNode(T&& data)  //不能提供默认构造 否则相当于两个默认构造: _data(move(data)), _prev(nullptr), _next(nullptr)
{}

记得也要修改insert,传过来的值是右值,否则调用的依旧是左值构造:

iterator insert(iterator pos, T&& val)
{//这里是在传入的迭代器前面插入数据Node* newNode = new Node(move(val));//...
}

所以在传递过程中,会存在退化现象。 

左右值可以来回切换:

十:完美转发(万能引用)

我们先来看代码:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }// 引用
// 传左值->左值引用
// 传右值->右值引用
template<typename T>
void PerfectForward(T&& t)
{// 模版实例化是左值引用,保持属性直接传参给Fun// 模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给FunFun(t);
}

我们想让PerectForward通过传入参数识别为是右值还是左值,这里我们这样写,因为存在退化,所以全部都是左值。 

int main()
{PerfectForward(10);           int a;PerfectForward(a);            PerfectForward(std::move(a)); const int b = 8;PerfectForward(b);			  PerfectForward(std::move(b)); return 0;
}

但是加上move又全部都是右值。

我们想让这个模板参数实现以下功能:

void PerfectForward(int& t)
{Fun(t);
}void PerfectForward(int&& t)
{Fun(move(t));
}void PerfectForward(const int& t)
{Fun(t);
}void PerfectForward(const int&& t)
{Fun(move(t));
}

这时就需要用到完美转发了,底层是一个类模板:

template<typename T>
void PerfectForward(T&& t)
{// 模版实例化是左值引用,保持属性直接传参给Fun// 模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给FunFun(forward<T>(t));  //完美转发
}

所以也叫做万能引用

至于为什么底层的insert等还是提供了两中参数的插入类型,是因为历史原因,原来的左值存在的太久了,也没必要修改。

总结:

本篇我们了解很多关于C++11的新特性,它提升了效率和简化了操作,之后我们还将学习一些其他的新特性,大家敬请期待!

相关文章:

  • 数据库MySQL学习——day8(复习与巩固基础知识)
  • cuDNN 9.9.0 便捷安装-Windows
  • Python读取comsol仿真导出数据并绘图
  • 【PostgreSQL数据分析实战:从数据清洗到可视化全流程】3.4 数据重复与去重(IDENTITY COLUMN/UNIQUE约束)
  • 软考-软件设计师中级备考 8、进程管理
  • 硬件加速模式Chrome(Edge)闪屏
  • React class 的组件库与函数组件适配集成
  • CSS 变量与原生动态主题实现
  • ES6/ES11知识点 续二
  • 高等数学第三章---微分中值定理与导数的应用(§3.6 函数图像的描绘§3.7 曲率)
  • FGMRES(Flexible Generalized Minimal Residual)方法
  • 关于MindVault项目测试报告
  • 【IP101】边缘检测技术全解析:从Sobel到Canny的进阶之路
  • 文章记单词 | 第60篇(六级)
  • 代码随想录day8: 字符串part01
  • WebRTC 服务器之Janus视频会议插件信令交互
  • STM32Cube-FreeRTOS任务调度与任务管理-笔记
  • Swift可以像Python一样在定义变量时省略var或者let?定义常量和变量的不同形式?const常量的不同形式?变量或常量修改?
  • 《解锁SCSS算术运算:构建灵动样式的奥秘》
  • 理解计算机系统_并发编程(1)_并发基础和基于进程的并发
  • 巴菲特股东大会精华版:批评拿贸易当武器,宣布年底交班
  • 海港通报颜骏凌伤停两至三周,国足面临门将伤病危机
  • 香港发生车祸致22人受伤,4人伤势严重
  • 玉渊谭天:美方多渠道主动接触中方希望谈关税
  • 中国海警位中国黄岩岛领海及周边区域执法巡查
  • 宁夏民政厅原厅长欧阳艳已任自治区政府副秘书长、办公厅主任