右值引用和移动语义

C++从一开始就引入了引用。引入引用是为了避免在函数调用值传递时产生复制的开销,又不想传指针把语法弄得复杂。引用是一个初始化时就固化了的指针,除了初始化,此后每一次使用都自动包含了*p的语义,不用显示写出来,从而减轻了程序员阅读指针表达式的负担。

有时候,函数不得不返回复杂的数据类型。比如大数运算的四则运算,重载了运算符后,无可避免要返回“数”类型。这种数据的复制会有不小的开销。为了避免复制,也想采用引用的办法来优化返回值传递。原始的想法是,让语言默认函数返回值匹配const type &的格式,从而让它能够引用传递。因为返回值是个临时存在,修改返回值无意义,站在这个角度上看,用const修饰它是合理的。

先看个不完善的写法:

class NumX {
public:
     list<int> l;
public:
	NumX operator+(NumX &);
};

上面重载的+运算符,对于普通的变量+尚可,但不能接受
d=a+(b+c);
这样的式子。因为(b+c)的返回值是右值NumX ,不能匹配参数中的NumX&。如果能让operator+()返回引用,则可匹配,可是语义规定它不能这样返回。改正的方法是把这个参数引用的目标说明为const的。

class NumX {
public:
     list<int> l;
public:
	NumX operator+(const NumX &);
};

const引用虽然解决了参数匹配问题,也避免了返回值再次复制的问题。但这只是问题的前半部。const对参数施加了常量约束,因此在函数中不能修改参数引用的对象,这不仅包括不能改动对象的数据,也包括不能调用对象的非常量成员函数。对于没写几个const成员函数的类型,这样的要求几乎不能用了。

这个副作用很大…不过其实是能用的…(抬杠中)…,就是在函数中用强制类型转化去掉const约束。

NumX NumX::operator+(const NumX &x)
{
	NumX &x2 = const_cast<NumX&>(x);
	...
}

对于operator+可以不用动参数对象的值。对于operator=就要考虑了。现在考虑上。这是跟operator+同样格式的另一个重载运算符。如果在调用operator=过程中参数匹配的是函数返回值,藏在返回值中的那个list,反正下一步就会丢掉的,何不把它的内容链也直接拿过来呢,把它弄成空list,对析构也没有影响。所以想这样写:

NumX& NumX::operator=(const NumX &x)
{
	NumX &x2 = const_cast<NumX&>(x);
	l.swap(x2.l);
	return *this;
}

但是这样不严谨。const NumX &x还要匹配真const NumX对象,遇到真的就麻烦了。不允许真的const NumX对象和函数返回值同时出现在这里。这种情况必须告诉每一个使用这个类的人。不然就等着出错。反之如果想允许并存就要增加额外信息量,就不能再用operator=运算符重载了,只能这样写:

NumX& NumX::non_operator_assign(const NumX &x, bool retvalue)
{
	if(retvalue) {
		NumX &x2 = const_cast<NumX&>(x);
		l.swap(x2.l);
	}
	else {
		l= x.l;
	}
	return *this;
}

这样问题总算解决了。但也到了引入右值引用时候了。现在通过引入右值引用做参数来重载它们:

NumX NumX::operator+(NumX &&x);
NumX NumX::operator=(NumX &&x);

直接匹配函数返回值,就避免了各种不规范的技巧:

class NumX {
public:
     list<int> l;
public:
	NumX operator+(const NumX &);
	NumX operator+(NumX &&);
	NumX operator=(const NumX &);
	NumX operator=(NumX &&);
public:
	NumX(const NumX &x);
	NumX(NumX &&x);
};

函数返回值优先匹配右值引用,所以可以和真的const 引用并存。这次真解决了问题。右值引用的第一个功能是解决参数匹配问题。这里顺带提一下,复制构造函数和赋值运算在使用右值引用方面非常相似,如法炮制即可,不重复说了。

右值引用的第二个功能是语义。右值引用暗示它引用的目标是来自函数的返回值,是个临时存在,因此想从那里拆点什么东西拿来用是完全可以的。所以直接就拆了:

NumX& NumX::operator=(NumX &&x)
{
	l.swap(x.l);
	return *this;
}

NumX& NumX::operator=(const NumX &x)
{
	l=x.l;
	return *this;
}

这就是右值引用暗示了它的移动语义。明显的,移动有个条件,里面必须有个指针,并且指向一个已经分配好的数据资源。如果连个指针都没有就算了,暗示等于白说。

那么标准库的forward和move模板是怎么回事呢。到目前为止,展示的右值引用语义的函数对参数拥有管辖权。实际碰到的情况不完全是这样。比如参数是某种表结构的node,在node内嵌了一个实际数据的对象,或者参数是pair模板这类情况,把几个实际数据对象粘贴在一起,这时需要把右值对象的一个或多个部分转发给对数据有管辖权的函数处理。处理时需要保持移动语义。右值引用的对象作参数传递时,不能直接保持右值属性。这时就要用到forward和move模板。如果是把参数分解后转发,就用move模板,如果参数完整转发就用forward模板。在普通代码中,参数又是确定右值引用的,都用move转发也没问题。但是考虑到在模板代码中用到forward和move,会遇到引用折叠问题,那时forward和move可能就不一样了,所以还是按照惯例使用比较好。注意转发处理完毕时,回原函数中,这时,原来右值引用的对象有可能已经被转发去的函数破坏掉了,这个值不能再用了。

最后总结一下,右值引用也不是处处都要用到,主要还是用来优化赋值运算和复制构造函数这类操作的。如果成员含有指针数据,则效果良好,如果没有,只剩下匹配返回值的作用。另外右值引用也不是唯一的办法,如果成员含有指针数据,对指针的数据资源通过引用计数管理,而不用右值引用这一套,也可以非常高效。

  • 24
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值