&& 右值引用

来源:互联网 发布:淘宝库存不足 编辑:程序博客网 时间:2024/05/16 06:33

c++11中的新概念,主要解决了移动语义和完美转发

我们平常使用的引用都是指左值引用。
以下是我经常用到的手法:
void func( int& v);
void func( std::string& s);

下面是一个测试例子

// =======================================================================================// rvaluevoid func ( int& i ) {  std::cout << "func(int& i) = " << i << std::endl;}void func ( std::string& s ) {  std::cout << "func(std::string& s) = " << s << std::endl;}void rvalue () {  std::cout << std::endl << std::endl << "-------------------------- rvalue test------------------------" << std::endl;  int i = 123;  std::string s = "hello";  func ( i );  // ok  func ( s );  // ok  func ( 1 );  // error : error C2665: “func”: 2 个重载中没有一个可以转换所有参数类型}// =======================================================================================

反正上面是出了问题,我么先了解一下基础知识在来回答上面的问题。
左值(lvalue)和右值(rvalue)是从c继承过来的概念,在c++11之后,新标准基于这两个概念新增了部分特征(右值引用,用来解决移动和转发语义)。
什么是左值、右值?
通俗的将:=左边的是左值,右边的是右值。
lvalue和rvalue的前缀怎么理解,left和right?好吧,l表示location,r表示read。
location表示可以在内存中寻址,可以被赋值,read表示可以直接知道值
什么是临时变量?
对于全局变量、局部变量,我们都清楚,那么什么是临时变量,什么时候出现。
怎么交换两个数?建一个临时变量和第一个交换,两个变量交换,临时变量和第二个交换。sorry,这只是我们业务逻辑上的临时变量,确切的说,只是我们创建的一个局部变量而已,那对于程序(或者说运行中的程序来说)什么才是临时变量?网上有位大神是这么解释的:编译期由编译器根据程序需要自动生成的,在运行期跑的时候是真实存在的,而主要发生在两个地方:函数传参发生类型转换时、函数返回值时。
比如说函数调用时,我们传一个字面量过去;或者函数返回一个字面量时;或是表达式中发生了类型转换时。

在c++中,临时对象不能作为左值,但可以作为常量引用const &
看下面的

  ++i = 3;  // ok  i++ = 3;  // error C2106: “=”: 左操作数必须为左值

++i:i先自增1,++i之后还是指向i的对象
i++:先把i自增之后的值丢给一个临时变量,自己去玩泥巴去了,i++之后指向的是那个临时变量。如果放在单条语句中,这两个没啥区别。

好了,现在看看上面出问题的地方func(1); 这个函数需要的是一个左值,但是字面量丢过去,她也只能放在一个临时变量中,而这个临时变量不能作为一个左值存在,就出了问题。

简单解决一下:

void func ( int&& i ) {  std::cout << "func(int&& i) = " << i << std::endl;}

或是用func ( const int& i )

好吧,这都不是今天要说的重点,重点是const int&& i;
&& 和 &一样都是引用,&&是新标准弄出来的,称为 右值引用

无名右值引用
一般由static_cast < T&& >(t)转换操作转换而来
也可以用标准库提供的std::move()来将左值转换成右值引用

带名右值引用
T&& 这是一个左值,只不过她的类型是右值引用,只能绑定右值
额外说一句:如果类型是T&& 且这个T类型无需推导即可确定,那么这个变量称为带名右值引用;如果这个T类型需要推导,那么这个变量称为转发型引用。

总结一下:
1、新标准为c++带来了一个新的左值类型:带名右值引用;带来了一个新的右值类型:无名右值引用
2、左值引用(就是以前的引用)可以绑定左值,也可以绑定右值;右值引用只能绑定右值(新型右值类型(右值引用)、传统右值类型(临时对象))。

A a;
A&& b = static_cast< A&&>(a);
A&& c = std::move(a);
创建两个无名引用,用来初始化两个右值引用对象

A& d = a;
A& e = b;
左值引用可以绑定左值

const A& f = c;
const A& g = A();
const引用(若无特殊说明,引用二字都是指传统的左值引用)还可以绑定临时变量

A&& h = A();
右值引用也可以绑定临时变量

如果重载存在const引用和右值引用,新标准规定右值引用参数绑定的函数优先级高于const引用

// =======================================================================================// rvaluevoid func (const int& i ) {  std::cout << "func(int& i) = " << i << std::endl;}void func ( int&& i ) {  std::cout << "func(int&& i) = " << i << std::endl;}void func ( std::string& s ) {  std::cout << "func(std::string& s) = " << s << std::endl;}void rvalue () {  std::cout << std::endl << std::endl << "-------------------------- rvalue test------------------------" << std::endl;  int i = 123;  std::string s = "hello";  func ( i );  // ok  func ( s );  // ok//  ++i = 3;  // ok//  i++ = 3;  // error C2106: “=”: 左操作数必须为左值  func ( 1 );  // 传过去是一个右值,根据新标准规定调用void func ( int&& i )  int&& a = std::move ( i );  func ( a );  // 左值 别忘了a是左值,类型是右值引用  func ( static_cast<int&&>( i ) );  // 右值}// =======================================================================================

结果如下:
————————– rvalue test————————
func(int& i) = 123
func(std::string& s) = hello
func(int&& i) = 1 // 虽然两个重载都满足,但是新标准规定右值引用绑定的优先级高于const左值引用
func(int& i) = 123
func(int&& i) = 123

接下来看看右值引用在移动构造和移动赋值中的应用
什么是移动语义?
可以对比拷贝语义对着看

目的:
移动:接管另一个对象所有外部资源的所有权
拷贝:复制另一个对象所有外部资源,并接管新生资源的所有权

过程:
移动:
释放this资源
将this对象的指针或句柄指向that对象拥有的资源
将that对象中指向资源的指针或句柄置空
拷贝:
释放this资源
拷贝that拥有的资源
将this对象的指针或句柄指向新生资源
如果that对象时临时对象(右值),在拷贝完之后,that对象会被释放

move constructor和move assignment
copy constructor和copy assignment的区别:
构造函数相比赋值函数来说,因为this对象为空,所以不需要进行this资源释放(第一步)

如果that是左值,移动和拷贝不能替换;如果是右值,此时移动可以代替拷贝,好处是完成相同的目标的同时,少用了一次拷贝和释放。
下面是网上找的一个例子,

#include <iostream>class MemoryBlock{public:    // 构造器(初始化资源)    explicit MemoryBlock(size_t length)        : _length(length)        , _data(new int[length])    {    }    // 析构器(释放资源)    ~MemoryBlock()    {        if (_data != nullptr)        {            delete[] _data;        }    }    // 拷贝构造器(实现拷贝语义:拷贝that)    MemoryBlock(const MemoryBlock& that)        // 拷贝that对象所拥有的资源        : _length(that._length)        , _data(new int[that._length])    {        std::copy(that._data, that._data + _length, _data);    }    // 拷贝赋值运算符(实现拷贝语义:释放this + 拷贝that)    MemoryBlock& operator=(const MemoryBlock& that)    {        if (this != &that)        {            // 释放自身的资源            delete[] _data;            // 拷贝that对象所拥有的资源            _length = that._length;            _data = new int[_length];            std::copy(that._data, that._data + _length, _data);        }        return *this;    }    // 移动构造器(实现移动语义:移动that)    MemoryBlock(MemoryBlock&& that)        // 将自身的资源指针指向that对象所拥有的资源        : _length(that._length)        , _data(that._data)    {        // 将that对象原本指向该资源的指针设为空值        that._data = nullptr;        that._length = 0;    }    // 移动赋值运算符(实现移动语义:释放this + 移动that)    MemoryBlock& operator=(MemoryBlock&& that)    {        if (this != &that)        {            // 释放自身的资源            delete[] _data;            // 将自身的资源指针指向that对象所拥有的资源            _data = that._data;            _length = that._length;            // 将that对象原本指向该资源的指针设为空值            that._data = nullptr;            that._length = 0;        }        return *this;    }private:    size_t _length; // 资源的长度    int* _data; // 指向资源的指针,代表资源本身};MemoryBlock f() { return MemoryBlock(50); }int main(){    MemoryBlock a = f();            // 调用移动构造器,移动语义    MemoryBlock b = a;              // 调用拷贝构造器,拷贝语义    MemoryBlock c = std::move(a);   // 调用移动构造器,移动语义    a = f();                        // 调用移动赋值运算符,移动语义    b = a;                          // 调用拷贝赋值运算符,拷贝语义    c = std::move(a);               // 调用移动赋值运算符,移动语义}

对于转发型引用多说两句:
转发型引用由以下两种语法产生:
auto&&
在模版函数中参数被声明为T&&
这两种都需要推导,都属于转发型引用

好了 再回到我们的移动语义
有一点需要注意的是:移动构造的参数不是const,从上面的例子也可以看到这一点;移动赋值也是一样的。

当=右边是一个右值或是用一个右值来初始化对象时,移动语义最适合了。
当使用std::move之后并不会改变任何事,只是返回一个右值引用(无名)

0 0