zoukankan      html  css  js  c++  java
  • C++11:移动语义与完美转发

    转自

    https://www.cnblogs.com/jianhui-Ethan/p/4665573.html

    C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference)。加入右值引用的动机在于效率:减少不必要的资源拷贝。考虑下面的程序:

    std::vector<string> v;
    v.push_back("string");

    首先看push_back:

    push_back(const T& x)

    实际上传进来的是引用

    而在push_back中构造时,使用的是construct:

    void construct(T1* p,const T2& value)
    {
      new(p) T1(value);  
    }
    

      T1*为迭代器所指的位置,而构造的值value也是引用。T1为迭代器类型,而p为该迭代器所处的地址,也就是说construct实际上是在迭代器所属位置上,用value来构造一个T1类型的迭代器。

    这里值得要注意的是,容器对其中存储元素的管理都是在堆上的(用自由存储区更精确),也就是说容器本身可能是在栈上,但它管理的元素一定是在堆上。

    在回到上面的过程:

    首先"string"这是一个char*指向的区域,而push_back调用的实际上里面是一个string&变量,因此首先进行隐式转换,调用string的string(const char*)这个含参的构造函数。

    而这个生成的变量其实上是一个临时变量。

    push_back可以看到他是传参的,因此这个临时变量直接被传进去而没有被拷贝构造。

    到目前为止,使用了隐式转换,而这次隐式转换调用了含参(char*)的string的构造函数。

    进入之后进入construct,而construct也是传引用,因此这里也没有调用构造。

    但是在construct内部,在new时,使用了T1的含参构造,相当于在堆上利用这个临时变量构造了一个T1类型的变量

    在这里调用了一次构造。

    而由于vector的迭代器是原始指针,因此这里T1与T2是同一种类型(当然对其他容器就不一定是这样了)

    因而调用的实际上是拷贝构造

    在堆上构造完毕后,construct退栈,push_back退栈,然后这个临时变量因为离开作用域而被析构

    实际上这个过程经历了一次含参构造,一次拷贝构造,一次析构

    std::vector<string> v;
    v.push_back("string");
    

      

    移动语义:

    上面程序操作的问题症结在于,临时对象的构造和析构带来了不必要的资源拷贝。如果有一种机制,可以在语法层面识别出临时对象,在使用临时对象构造新对象(拷贝构造)的时候,将临时对象所持有的资源『转移』到新的对象中,就能消除这种不必要的拷贝(将“string”直接转移给construct构造处的新对象)。这种语法机制就是『右值引用』,相对地,传统的引用被称为『左值引用』。左值引用使用 ‘&’ 标识(比如 string&),右值引用使用 ‘&&’ 标识(比如 string&&)。顺带提一下什么是左值(lvalue)什么是(rvalue):可以取地址的具名对象是左值;无法取值的对象是右值,包括匿名的临时对象和所有字面值(literal value)。有了右值的语法支持,为了实现移动语义,需要相应类以右值为参数重载传统的拷贝构造函数和赋值操作符,毕竟哪些资源可以移动、哪些只能拷贝只有类的实现者才知道。对于移动语义的拷贝『构造』,一般流程是将源对象的资源绑定到目的对象,然后解除源对象对资源的绑定;对于赋值操作,一般流程是,首先销毁目的对象所持有的资源,然后改变资源的绑定。另外,当然,与传统的构造和赋值相似,还要考虑到构造的异常安全和自赋值情况。作为演示:

    其中,重载了String的"=",使遇到使用右值来进行构造时,转移对象的资源。

    class String {
    public:
        String(const String &rhs) { ... }
        String(String &&rhs) {
            s_ = rhs.s_;
            rhs.s_ = NULL;
        }
        String& operator=(const String &rhs) { ... }
        String& operator=(String &&rhs) {
            if (this != &rhs) {
                delete [] s_;
                s_ = rhs.s_;
                rhs.s_ = NULL;
            }
            return *this;
        }
    private:
        char *s_;
    };
    

      值得注意的是,一个绑定到右值的右值引用是『左值』,因为它是有名字的。考虑:

    其中,D的构造函数重载了一个右值引用版本,在这个版本中使用了初始化列表来初始化基类,但B(rhs)中rhs实际上是个左值,也就是说对B调用构造时将会调用拷贝构造版本,而不是B的移动构造版本

    class B {
    public:
        B(const B&) {}
        B(B&&) {}
    };
    class D : public B {
        D(const D &rhs) : B(rhs) {}
        D(D &&rhs) : B(rhs) {}
    };
    D getD();
    D d(getD());
    

      上面程序中,B::B(B&&) 不会被调用。为此,C++11 中引入 std::move(T&& t) 模板函数,它 t 转换为右值:

    class D : public B {
        D(D &&rhs) : B(std::move(rhs)) {}
    };
    

      

    绑定规则

    引入右值引用后,『引用』到『值』的绑定规则也得到扩充:

    • 左值引用可以绑定到左值: int x; int &xr = x;
    • 非常量左值引用不可以绑定到右值: int &r = 0;
    • 常量左值引用可以绑定到左值和右值:int x; const int &cxr = x; const int &cr = 0;
    • 右值引用可以绑定到右值:int &&r = 0;
    • 右值引用不可以绑定到左值int x; int &&xr = x;
    • 常量右值引用没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着『修改』)。

    考虑下面这段代码:

    当调用不同的构造函数时,会输出不同的语句

    class HasPtrMem{
    public:
    	HasPtrMem():d(new int(3))
    	{
    		cout<<"Construct:"<<++n_cstr<<endl;
    	}
    	HasPtrMem(const HasPtrMem& tmp):d(new int(3))
    	{
    		cout<<"copy constructor:"<<++n_cptr<<endl;
    	}
    	HasPtrMem(HasPtrMem&& h)
    	{
    		d=h.d;
    		h.d=nullptr;
    		cout<<"move constructor:"<<++n_mvtr<<endl;
    	}
    	~HasPtrMem()
    	{
    		delete d;
    		cout<<"destruct:"<<++n_dstr<<endl;
    	}
    	int * d;
    	static int n_cstr;
    	static int n_dstr;
    	static int n_cptr;
    	static int n_mvtr;
    };
    int HasPtrMem::n_cstr=0;
    int HasPtrMem::n_dstr=0;
    int HasPtrMem::n_cptr=0;
    int HasPtrMem::n_mvtr=0;
    
    HasPtrMem GetTemp()
    {
    	HasPtrMem n;
    	return n;
    }
    int main() {
    	
    	HasPtrMem a=GetTemp();//a
         //HasPtrMem a;//b1
         //a=GetTemp;//b2
         //GetTemp//c
       cout<<endl; }

      我们分别执行a,b1与b2和c:

    执行a时:

    分析一下这个过程,首先进入GetTemp,生成一个n,此处调用默认构造函数

    然后返回n,在返回n的时候,理论上应该生成一个临时变量,此处应该是编译器优化过了,将a当做这个临时变量直接进来构造。

    而此处是调用的移动构造函数,尽管n是一个左值,难道说对临时变量的构造都是调用的移动构造函数吗???

    构造完毕后,析构掉这个n

     执行b1,b2时:

    分析一下这个过程,首先创建a,此时调用默认构造函数。

    然后进入GetTemp内部,首先创建n,此处调用默认构造函数。

    由于我们这里是使用的赋值函数,因此不会像上面一样进行优化。

    首先用n移动构造一个临时变量,构造完毕后n析构。

    然后用这个临时变量复制给a,由于我们没有重载=,因此此处用的默认的赋值函数。

    最后临时变量析构

     执行c时:

    我们分析一下这个过程

    注意这里与a的区别,由于这里只调用了GetTemp,因此编译期是无法优化的。

    首先创建n,调用默认构造函数

    然后调用移动构造去构造临时变量

    然后n析构

    最后临时变量析构

     对于b1,b2,当我们加上移动赋值函数后:

    HasPtrMem& operator=(HasPtrMem&& s)
    {
    	d=s.d;
    	s.d=nullptr;
    	cout<<"move operator=:"<<endl;
    	return *this;
    }

    结果为:

    我们分析这个过程,前面不表。

    在临时变量调用移动构造函数构造完毕后,n析构。

    然后对a,调用了移动赋值函数,然后临时变量析构。

     看完上面,有一个问题是:

    难道对临时变量的构造一定使用移动构造函数吗?这里的n是一个左值!

  • 相关阅读:
    日期和时间运算:上月最后一天
    SY全局系统字段
    内表、结构赋值转换规则
    基本类型赋值转换规则表
    嵌套结构使用:struc1-struc2-XXX
    TYPES、DATA、TYPE、LIKE、CONSTANTS、STATICS、TABLES
    ABAP WRITE、WRITE TO、FORMAT语句
    ABAP DESCRIBE语句
    数据词典与ABAP类型映射
    Field+offset(len)
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11539946.html
Copyright © 2011-2022 走看看