目录
左值引用和右值引用
左值引用与右值引用比较
编辑
右值引用使用场景和意义
左值引用的使用场景:
右值引用和移动语义
移动拷贝
移动赋值
右值引用引用左值及其一些更深入的使用场景分析
完美转发
完美转发维持值自身属性
完美转发的使用场景
左值引用和右值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式,如变量名或解引用的指针。
- 左值可以出现赋值符号的左边,左值还可以出现在赋值符号右边。
- 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);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}
注意:
以上案例中的 10就是一个常量值,x + y,fmin函数它们的返回值是一个临时变量,也是一个右值
这些常量和临时变量并没有被实际的储存起来(常量的创建并不会开空间,需要开辟空间存储右值的是左值,而临时变量是一种特殊的右值,后面会进行讲解),这就是为什么右值不能取地址,而左值可以取地址的原因
右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址
例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1去引用
这里了解即可
无论左值引用还是右值引用,都是给对象取别名
左值引用与右值引用比较
左值引用总结:
左值引用只能引用左值,不能引用右值, 但是const左值引用既可引用左值,也可引用右值。
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a;// ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}
右值引用总结:
右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值。
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a; // 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
我们来尝试一下,看看使用右值引用和const左值引用作为形参能否构成函数重载
void func(int&& a)
{cout << "int&& a" << endl;
}void func(const int& b)
{cout << "const int& b" << endl;
}int main()
{ func(10);return 0;
}
通过以上代码分析右值引用和const左值引用作为形参可以构成函数重载,使用右值进行函数调用时,编译器会优先选择有右值引用的那个版本
右值引用使用场景和意义
对于const 左值引用来说,它既能接收左值也能接收右值,但是对于左值引用来说,在某些特殊场合,也有它解决不了的问题例如:传值返回
为了解决左值引用的缺陷,C++委员会在C++11版本中引入了右值引用来弥补左值引用的不足
以下string是使用深拷贝实现的简化版本
namespace nxbw
{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){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);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};
}
左值引用的使用场景:
左值引用的使用场景和价值是什么呢?
1.做参数,2.作返回值,减少拷贝构造减少消耗,提高效率
作为返回值左值引用并不能在所有场景中都适用
void func1(nxbw::string s)
{}
void func2(const nxbw::string& s)
{}
int main()
{nxbw::string s1("hello world");//func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);//string operator+=(char ch) 传值返回存在深拷贝//string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}
左值引用的价值就是减少拷贝构造,提高效率
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。
例如:nxbw::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(编译器优化,如果是一些旧一点的编译器可能是两次拷贝构造)。
nxbw::string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}nxbw::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;
}
我们在main函数中使用string对象接收由_to_string产生的临时变量时,不可避免的会进行深拷贝
int main()
{nxbw::string s1 = to_string(100);return 0;
}
右值引用和移动语义
为了解决上述场景中的深拷贝问题,我们只需要给模拟实现的string引入移动赋值和移动构造(移动语义)即可
移动拷贝
移动拷贝上一是构造函数,该函数的参数是右值引用,它与使用左值引用的构造函数构成函数重载
移动构造的本质就是夺取右值的资源,转换到自身,这样就避免了对右值的拷贝构造(深拷贝)
简单来理解就是窃取别人的资源化为己用
template<class T>
class string
{
public:// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}private:char* _str;size_t _size;size_t _capacity;
};
移动拷贝和拷贝构造的区别
对于拷贝构造而言,它会对资源进行深拷贝
对于移动构造而言,它只需要调用swap进行资源的转移,所以调用移动构造的代价比调用拷贝构造代价要小
特殊的右值:不是说右值不能取地址吗,临时数据可以取地址为什么是右值呢,它是一种特殊的右值,在以上场景中如果我们没有使用移动拷贝,它会调用拷贝构造构造一个临时变量代替str进行返回,临时变量的生命周期很短暂,使用之后就会被析构进行销毁,我们将这种的值称为:将亡值
将亡值都要销毁了,那还不如将自己的资源换给需要的人,所以编译器会将这这种将亡值识别成右值,又称:“右值 - 将亡值”,这样临时数据就会调用移动拷贝,将它的值转给别人
编译器优化:
在C++11出来之前,实际上我们在传值返回的时候,编译器会调用拷贝构造,构造出一个临时对象,然后编译器会再调用一次拷贝构造将值拷贝给s1
上面是编译器没优化的场景
编译器优化之后(优化条件:连续的 构造/拷贝构造 在同一个步骤中),减少了一次拷贝构造(深拷贝)
大部分编译器为例性能都对着重情况进行了优化处理
C++11出来之后,编译器的优化仍然有效
编译器没有优化之前,编译器会调用移动构造将str的数据移动到临时对象中,然后编译器会再次调用移动拷贝将临时对象的数据转给s1
编译器优化之后,编译器会优化将两次移动移动拷贝优化成一次移动拷贝
编译器优化之后将临时对象给优化掉了,现在的问题是str使用的是移动构造呢?
按照常理来说str可以取地址是左值,应该调用拷贝构造
因为在C++11之前还没有右值的时候,世界上C++的代码量已经是非常庞大了,不可能说去人为的给所有这种情况的代码全都使用上move函数,这种时间成本是非常大的,为了快速提高C++11的使用率,C++委员会将该情况下的左值都默认套上了move函数
这种情况下的str和move(str)是等价的
str出了作用也很会被销毁,正好符合将亡值特性,编译器将str识别成了右值-将亡值,原本应该代替str返回的临时对象被编译器给优化掉了,现在由str返回,str在销毁之前会来进行拷贝
C++11之后,其实这种编译器优化行为是可有可无的,第一次使用移动拷贝将str转给临时变量,然后第二次调用移动拷贝将数据转给s1,在这个过程中消耗是不大的
还有一种不使用编译器优化的场景,如果我们没有使用函数返回值来拷贝构造一个对象,而是使用已经定义过的对象来接收返回值
这种行为编译器不会对这种情况进行优化,编译器第一次调用拷贝构造构造出一个临时对象,第二次调用赋值运算符重载函数将临时对象的数据赋值给s1,在C++11没有出来之前,这里会存在两次深拷贝,在C++11出现之后这里的拷贝构造会被移动拷贝给代替,但还是会出现一次深拷贝,所以我们还需要实现一个移动赋值
这里需要注意的是:对于这种返回局部变量的函数,就算我们没有使用对象来接收,单单调用它也会有一次深拷贝,因为该函数有一个返回值,它需要深拷贝一个临时对象,来确保外部可以及时接收
移动赋值
移动赋值是一个赋值运算符重载函数,它的参数是右值引用类型的,和移动构造一样,内部复用swap函数交换右值对象的资源,占为己用,然后将自己不需要的资源转给该对象去调用析构释放
template<class T>
class string
{
public://移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}private:char* _str;size_t _size;size_t _capacity;
};
移动赋值和operator=的区别
在移动拷贝没有出来之前,我们在string类中使用的是深拷贝的operator=,通过const左值引用作为参数,接收左值和右值
移动拷贝出来之后,移动拷贝就相当是深拷贝operator=的重载,它使用右值作为参数,专门接收将亡的右值,如果赋值时传入的是右值那么它就会去优先匹配与它最适合的函数(右值引用的赋值运算符重载)
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
在C++11之后,容器中就新增了移动赋值和移动拷贝
string
vector
注意:对于一些浅拷贝的类,不需要实现移动拷贝和移动构造,没有意义,例如:Date日期类,成员变量全部都是内置类型,就算深拷贝也不会占用多大空间,但是像map<string, string>这样的类型,移动拷贝就很有必要了
右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
int main()
{int a = 10;int&& c = a; //errormove(a); //并不会该变a的属性int&& rr1 = a; //errorint&& rr2 = move(a);return 0;
}
move函数它会给你返回一个右值的引用,并不会改变原来变量左值的属性
右值引用版本的插入函数
在移动构造和移动拷贝出来之后STL容器的接口也增加了移动拷贝和移动赋值,就算插入接口的右值引用版本
以list容器的push_back接口为例:
当list中储存的是string对象时,那我们使用push_back向接口中插入元素的情况有以下几种:
int main()
{//1.list<bit::string> lt;bit::string s1("111111111111111111111");lt.push_back(s1);//2.cout << endl << endl;bit::string s2("111111111111111111111");lt.push_back(move(s2));//3.cout << endl << endl;lt.push_back("22222222222222222222222222222");return 0;
}
第一种情况:使用的是已经构造好的string对象向链表中进行插入,s1是一个左值,它插入进list容器中,首先需要构造一个结点,然后使用传入的对象,调用结点类的构造函数的对结点进行构造,string类对象进行初始化时,会调用它自己左值引用版本的拷贝构造进行初始化,是一个深拷贝
第二种情况:先使用move函数将一个string类的左值进行右值转换,然后插入到list容器中,由于我们传入的是右值,所以string在进行初始化时会调用它左值引用版本的拷贝构造函数
第三种情况:直接将字符串插入到list容器中,首先字符串和string类型(自定义类型)类型不一致,这里会进行一个隐式类型转换,调用string的构造函数,将字符串类型转化成一个string类的匿名对象,我们知道匿名对象的生命周期是非常短暂的,基本上是即用即毁,所以这里编译器将它识别成了右值-将亡值,由于是右值,在给结点进行初始化的时候它会去调用string类右值引用版本的拷贝构造也就是移动拷贝,即对右值进行资源掠夺,这样的拷贝代价是非常低的
完美转发
在模板中&&代表的是万能引用而不是右值引用,它既能接收左值也能接收右值,如果实参是左值,它就算左值引用(引用折叠),如果实参是右值,它就是右值引用
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
我们接着看下面的场景:我重载了四个func函数它们的参数分别是 左值引用,const左值引用,右值引用,const右值引用,然后再主函数main中分别使用这四种类型的变量来调用这四个重载函数我们来看看结果怎样
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(t);
}int main()
{PerfectForward(10);// 右值int a;PerfectForward(a);// 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
运行结果:
可以看到为什么调用的全部都是左值?这里是不是就可以证明右值在经过一次参数传递之后属性会变成左值
我们来看下面一个场景,来解释上面的问题
int&& rrr = 10;
rrr++;
cout << &rrr << endl;
严格意义上来说,右值是不能被取地址和进行修改的,我们可以这样理解右值10是没有开空间的,我们帮10起了别名rrr,rrr存放常量10需要开辟空间,在这个过程中,10被放到了rrr所开辟的空间中,这时这个右值就可以被取地址,而且可以被修改,完全符合左值的特性,所以编译器,就将这个实参识别成了左值
完美转发维持值自身属性
想维持数据在传参时的数据我们需要使用forward函数
template<typename T>
void PerfectForward(T&& t)
{Fun(forward<T>(t));
}
由上图可知,使用forward函数之后传入函数的右值就会取匹配右值版本的func,这就是完美转发的价值
完美转发的使用场景
string模拟实现简易版本:
namespace nxbw
{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){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << 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};
}
list模拟实现的简易版本,以下代码分别提供了右值引用版本和左值引用版本的push_back和insert接口
namespace List
{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;}void PushBack(T&& x){Insert(_head, x);}void PushBack(T& x){Insert(_head, x);}void Insert(Node* pos, T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;};
}
结合以上两个场景,我们来看看完美转发的使用价值
在主函数main我使用list容器 + string类型的方式进行对象的创建,并且使用了两种方式进行数据的插入
int main()
{List::List<nxbw::string> lt;nxbw::string s1("1111");lt.PushBack(s1);lt.PushBack("2222");return 0;
}
在左值引用的版本中,push_back接口会去复用insert接口实现它的功能,它会去和左值版本的insert进行对接,在insert接口中,它会创建一个新结点,会去调用string类的构造函数,在下面的操作中,它会调用string类中的operator=将新构造出来的string对象赋值给新结点上的数据区,在赋值的过程中,它会进行一次深拷贝,因为在进行赋值的过程中它会创建一个临时对象
为了解决以上问题,这里我们提供了右值引用版本push_back和insert接口,在右值引用的版本中,push_back会去复用右值版本的insert接口,同样的在给新结点的数据区赋值时会调用string类中的operator=,但是这时会使用右值引用版本的移动赋值,在此期间,不会进行深拷贝,大大的提高了效率
但是实际上不会向我们上面所讲述的那样简单,使用push_back进行元素插入的过程中,它并不会去调用右值引用版本的insert进行移动赋值
我们上面说过当我们给一个右值取别名之后,这个右值的性质就改变了,当我们再去使用它的时候,编译器会将它识别成左值,然后去调用左值引用版本的insert,期间还是会进行一次深拷贝
在这种场景下,就可以体现我们forward函数的价值,我们可以将需要保留属性的数据,给它们套上forward函数,让它们去正确的调用,左值调用左值版本,右值调用右值版本
void PushBack(T&& x)
{Insert(_head, forward<T>(x));
}void Insert(Node* pos, T&& x)
{Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = forward<T>(x); // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;
}
在右值版本中,想保持右值属性调用右值版本的函数,那每次进行传参时,我们都需要进行完美转发,在实际的STL库中也是使用这种方法来维持右值属性的
注意:在push_back和insert的参数T&&是右值引用而不是万能引用,其原因就是参数T在list中就已经被实例化过了,在调用string中的接口时其中的参数T其实已经是实例化出来的类型了,并不是调用push_back和insert函数推理出来的