早先认识到的是左值和右值的区别,简单定义为赋值符号左侧的值为左值,而右侧的为右值,左值可以作为右值出现,但是右值不能作为左值出现。这种定义方式在教学中足够了,但是随着C++语言的进步,出现了需要对一些特殊右值操作的需要,这里主要指的是临时变量作为右值。在C++11标准中,对表达式做了如下的分类。

                         expression
                     	 ↙        ↘
                      glvalue     rvalue
                     ↙      ↘    ↙      ↘
                 lvalue     xvalue     prvalue

其中prvalue是pure-rvalue,比如常数、字符串常量,都属于这种纯右值,不做讨论。而lvalue就是此前我们认识的变量标识符对象,他们可以被多次修改。而xvalue则代表生命期很短的对象,例如在

char c = getchar();

表达式中,getchar返回后编译器会替我们构造一个看不见的char变量x,实际上c是被这个x赋值了,而当表达式结束后x的生命期就结束了,这里的x,就属于xvalue。而glvalue表示广义的左值,理解了这些概念之后很自然就会产生两个问题:

  1. 强行把xvalue作为左值有什么用呢,又不能改变它?
  2. 就算能改变它,又有什么意义呢?

带着问题继续看。这次我们定义一个复杂类型:mstr类,用于保存字符串,并为它定义拷贝构造函数和拷贝赋值函数。

class mstr
{
private:
	char *m_p;
public:
	mstr(const char *p = NULL):m_p(NULL){
		cout<<"in regular constructor:"<<static_cast<void*>(this)
      <<endl;
		if (p)
		{
			unsigned int nLen = strlen(p);
			this->m_p = new char[nLen + 1];
			strcpy(this->m_p, p);
			this->m_p[nLen] = 0;
		}
		else
		{
			this->m_p = new char[1];
			this->m_p[0] = 0;
		}
	}
	~mstr()
	{
		if (this->m_p)
		{
			cout<<"in destructor address:"<<static_cast<void*>(m_p)
                <<endl;
			delete []this->m_p;
			this->m_p = NULL;
		}
	}
	// copy
	mstr(const mstr & s){
		cout<<"in copy constructor"<<endl;
		if (!this->m_p)
		{
			delete []this->m_p;
			this->m_p = NULL;
		}

		unsigned int nLen = strlen(s.m_p);
		this->m_p = new char[nLen + 1];
		strncpy(this->m_p, s.m_p, nLen);
		this->m_p[nLen] = 0;
	}

	mstr & operator=(const mstr & s){
		cout<<"in copy assignment"<<endl;
		if (this == &s)
			return *this;

		delete []m_p;
		unsigned int nlen = strlen(s.m_p);
		this->m_p = new char[nlen + 1];
		strcpy(this->m_p, s.m_p);
		this->m_p[nlen] = 0;

		return *this;
	}
public:
	void print()
	{
		cout<<"address:"<<static_cast<void*>(m_p)<<", value:"
      <<m_p<<endl;
	}
};

拷贝构造函数的实现方式是将源对象设置为const引用,意为不会对源对象的值进行修改,当深拷贝发生时,假如源mstr对象保存了1MB的字符串,那么我们的左值mstr也需要new出1MB的空间保存字符串副本。但是,如果源对象将立即被销毁(如getchar的返回值),那这次构造就产生了一次1MB的数据拷贝成本和1次delete,这也是此前C++函数返回临时对象调用深拷贝产生的效率问题。这种效率的损耗在成百上千次的调用中会被放大。

定义对xvalue的引用,就是为了能让C++代码可以有机会抓住稍纵即逝的xvalue(右值引用),并将它的值‘偷’过来(move语义),比如将xvalue的1MB字符串首地址指针直接复制过来,并在复制完成后清空源对象的指针,注意这里说的是清空,不是delete。在这个过程里由于xvalue的值发生了变化,所以不是严格意义上的右值,它具有了广义左值的属性。这就是上面两个问题的答案。

了解一下这两个新特性的语法,右值引用的语法是两个’&’符号:

char && c = std::move(getchar());

move语义的表现就是std库中的move函数,它的入参是一个xvalue,而返回值就是一个右值引用。

move是如此简单以至于其实现可以只有一句话:

static_cast<remove_reference<decltype(arg)>::type&&>(arg);

描述完右值引用和move语义的来龙去脉,下面改写一下此前的mstr类,实现基于右值引用和move语义的拷贝构造函数

class mstr
{
private:
	char *m_p;
public:
	mstr(const char *p = nullptr):m_p(nullptr){
		cout<<"in regular constructor:"<<static_cast<void*>(this)
            <<endl;
		if (p)
		{
			unsigned int nLen = strlen(p);
			this->m_p = new char[nLen + 1];
			strcpy(this->m_p, p);
			this->m_p[nLen] = 0;
		}
		else
		{
			this->m_p = new char[1];
			this->m_p[0] = 0;
		}
	}
	~mstr()
	{
		if (this->m_p)
		{
			cout<<"in destructor address:"<<static_cast<void*>(m_p)
                <<endl;
			delete []this->m_p;
			this->m_p = nullptr;
		}
	}
	// copy
	mstr(const mstr & s){
		cout<<"in copy constructor"<<endl;
		if (!this->m_p)
		{
			delete []this->m_p;
			this->m_p = nullptr;
		}

		unsigned int nLen = strlen(s.m_p);
		this->m_p = new char[nLen + 1];
		strncpy(this->m_p, s.m_p, nLen);
		this->m_p[nLen] = 0;
	}

	mstr & operator=(const mstr & s){
		cout<<"in copy assignment"<<endl;
		if (this == &s)
			return *this;

		delete []m_p;
		unsigned int nlen = strlen(s.m_p);
		this->m_p = new char[nlen + 1];
		strcpy(this->m_p, s.m_p);
		this->m_p[nlen] = 0;

		return *this;
	}

	// move
	mstr(mstr && s){
		cout<<"in move constructor"<<endl;
		this->m_p = s.m_p;
		s.m_p = nullptr;
	}

	mstr & operator=(mstr && s){
		cout<<"in move assignment"<<endl;
		std::swap(m_p, s.m_p);
		return *this;
	}

public:
	void print()
	{
		cout<<"address:"<<static_cast<void*>(m_p)<<", value:"
      <<m_p<<endl;
	}
};

这次我们重载了move构造和move赋值操作符重载,由于move操作中需要修改形参的成员,所以形参不有const修饰符,这是区别于copy构造和赋值符重载的地方。

实际执行效果自行验证吧。

需要注意的是,std::move的参数,不能是左值,因为move拷贝构造返回后,入参的成员会被”掏空”。而左值作为变量描述符可能在后面的代码中继续会用到,所以应避免这种用法。在实际测试当中,vs2012编译器并没有禁止将左值传入move函数,而在xcode中则编译报错提示,rvalue reference cannot bind to lvalue。

右值引用和move的配合,解决了C++返回对象时的临时对象拷贝成本,对开发人员来说,只需要为自己的类型增加一个move构造和赋值符重载,开发成本极低但又能带来显著性能提升,是C++11中非常感人的新特性。

以上。