目录
前言
一、什么是左值、什么是右值
二、右值引用
1.右值引用与右值引用的一些性质
2.解释一下左值引用与右值应用于程序员之间的关系
3.右值引用与移动语义
4.右值引用右值后变成左值的必要性与完美转发
1.右值引用引用右值后变为左值属性的必要性
2.完美转发
Ⅰ.引用折叠(万能引用)
Ⅱ.完美转发
前言
C++中的左值引用解决了大部分的拷贝效率问题,但是还有着诸如:一个资源占用很大的局部对象返回时,会造成多次拷贝的问题,这对C++程序运行的效率,资源的使用都是较为沉重的打击,为了解决这一问题,C++11提出了右值引用并将这一概念引入到了STL中。本文就来解释一下,什么是右值引用,右值引用的优势在哪。
一、什么是左值、什么是右值
对于如下代码,a就是一个左值,0就是一个右值。具体一点左值右值最大的差异就在于:左值可以进行取地址操作,而右值不行。
int a = 0;
二、右值引用
1.右值引用与右值引用的一些性质
①左值引用只能引用左值
②const修饰的左值应用可以修饰右值
③右值引用只能修饰右值
④右值引用可以引用move的左值
⑤对右值进行右值引用会使引用褪去常性
2.解释一下左值引用与右值引用与程序员之间的关系
左值引用与右值引用一样如同一个触发时的开关,程序员对左值引用的构造函数、赋值拷贝、右值应用的构造函数、赋值拷贝函数进行定义,当用户向这些重载的函数们丢入一个数据的时候,编译器站出来帮忙选择要将这个数据放入那个函数中最能够节省效率和资源。对于一个左值输入,编译器会将使用左值引用的相关函数来对对象进行字段填充,对于一个右值输入,编译器会使用右值相关的函数来对对象进行字段填充。
3.右值引用与移动语义
右值引用的效率提升是通过对资源的剽窃来实现的,比如说一个由动态开辟的内存的对象,生命周期马上结束,这个时候我们的函数需要返回这个对象,在C++11到来前,通常是将这个对象做一次拷贝,而后将这个拷贝的对象的值在拷贝给接收返回值的变量(此处默认,不开启编译器优化功能)。假如我们在对这个局部对象返回的时候,可以直接将需要返回对象的资源指向指针重新定位到该函数外的对象,不让编译器去两次拷贝赋值,是不是就极大的提升了效率。而右值应用很大程度上是依靠移动语义来实现效率的提升的,也就是说移动语义是效率提升的具体实现。那么移动语义具体实现是什么样子的呢,请看如下代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>namespace test
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){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);}// s1.swap(s2)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){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}int main()
{test::string a = "111";test::string b = a;test::string c;c = a;test::string d(std::move(a));test::string e;e = "111";return 0;
}
我们自己定义一个string类型(就是类似C++STL库中的string,只不过我们手动实现一个简单版的,添加打印来查看代码的走向。),其他的类成员函数我们暂且不看,我们先将目光聚焦在这样几个函数:void swap(string& s)、string(string&& s)、string& operator=(string&& s),我们按照顺序暂且依次称呼这三个函数为swap函数,移动构造函数、移动赋值函数,我们通过上述代码不难看出swap函数的实现逻辑是:将传入对象的资源换出到this指针指向的对象中,这样就是实现来对应资源的转移。需要注意到是,如果将资源换入到当前对象中,被换出资源的对象会持有当前对象的数据,也就是说被换出资源的对象会析构换入对象的资源,所以对于移动构造我们应该对指针类型都应初始化为空指针类型,来保证换出资源对象的释放资源不会出错。
在观察移动构造和移动赋值函数,二者在实现的时候,都是使用右值引用类型作为参数输入,在具体函数实现的过程中封装了swap函数,这其实也就解释了移动语义的工作原理,是通过资源交换来实现效率提升的。运行上述代码进行测试有如下结果:
4.右值引用右值后变成左值的必要性与完美转发
1.右值引用引用右值后变为左值属性的必要性
在图5中的程序中,重载了函数func1_,函数func1调用该重载函数。但是调用的结果显示调用的是左值引用的重载函数,这一点其实我们在介绍右值引用性质的时候就已经阐述,关于这一点我们需要知道的是:
右值引用右值后,必然会变为左值属性,因为只有左值才可以被修改,能被修改的对象才可以进行资源指针指向的改变,才能将资源换出/换入。
2.完美转发
Ⅰ.引用折叠(万能引用)
引用折叠发生在对类对象使用泛型时,如下列代码所示。
template<class T>
void func1(T &&element1)
{func1_(move(element1));func1_(std::forward<T>(element1));
}
其中的折叠规则是:
当传入的是一个左值是,若程序员没有实现对应的版本时,该模板会自动推演生成一个左值版本的func1,当传入一个右值的时候,该模板会自动推演生成一个右值版本func1。
Ⅱ.完美转发
如何才能使func1函数保持传入参数的右值属性,进而调用右值引用呢?
方式一:可以对传入参数使用move函数使其转变为右值
方式二:对传入参数使用完美转发。完美转发可以通过一定的方法推导出传入内容是左值还是右值,进而对下一步的数据走向做判断。
如果只从本段代码来看方式一与方式二基本没有差异,但是我们再来看另一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>using namespace std;
template<class T>
void func1_(T&& element1_)
{cout << "右值引用" << endl;
}template<class T>
void func1_(T& element1_)
{cout << "左值引用" << endl;
}template<class T>
void func1(T &&element1)
{func1_(move(element1));func1_(std::forward<T>(element1));
}int main()
{int a = 0;cout << "传入左值" << endl ;func1(a);cout <<endl<<"传入右值" << endl;func1(0);return 0;
}
当我们传入一个右值的时候,诚然,代码没有问题。但是当我们传入一个左值的时候,方式一,就不能达到我们的要求。实际上,完美转发可以说是,为了正确转发左右值属性而诞生的。