C++中的CopyElision

函数返回对象的Copy Elision处理

以前C++函数返回一个对象的时候,涉及到对象的拷贝,为了提高效率,加入了copy elision的优化设计。

假如我们有一个Vector类,表示向量。

在C++的设计中,函数返回对象的时候其实返回的是对象的一份拷贝,然后用这个拷贝的对象去给外部使用。但从实际使用角度来说,生成的中间拷贝对象明显是不需要的,所以各大编译器在支持C++的时候,想出了一个优化方法,copy elision,顾名思义,拷贝消除,就是消除中间的拷贝过程,直接建立起数据桥梁!

 这些优化策略或称之为NRVO,named return value optimizatioin,或称之为RVO,return value optimation。二者的区别是,前者返回的变量具名(有名字),后者不具名(没名字)。如下面的两个函数,GetValue1返回a,a有名字,GetValue2返回Vector(),没有名字。

Vector GetValue1()
{
	Vector a;
	return a;
}
Vector GetValue2()
{
	return Vector();
}

不管具名还是不具名,它们都有一个相同的行为:为了返回对象给外部使用,需要拷贝一个中间的临时变量,这一步需要被优化消除掉,return value optimization,简称RVO,有名字的加个named前缀,叫做NRVO。

这种优化策略,能够带来很大的性能提升,但也有缺陷:如果一个类不支持拷贝(或者移动,C++11带来的新操作,下文直接说拷贝构造,不再赘述移动),就不能被copy elision!

啥?还有这种要求?

因为它们的目的是copy elision,所以本质上是对拷贝的处理,优化的前提是代码符合语言标准,而C++17之前,C++认为,这种return Vector()情况,因为涉及到临时变量的拷贝,所以Vector必须提供拷贝构造函数,否则类的设计是有问题的。所以C++17之前,RVO成立的前提是class有拷贝或者移动构造函数才行,否则编译报错!

而这种return Vector()代码,明眼人一看就知道,其实是可以不需要拷贝构造的嘛,优化之后,直接以Vector里面的()(这里是空)为参数在外部地址进行默认构造就行了,为啥一定要class提供拷贝构造呢?

于是C++17带来了更新,对于return Vector()这种情况,强制RVO,此时RVO的处理从编译器级别提升到了语言级别,中间没有任何临时对象的产生,因为没有临时对象产生了,当然也就不再需要class提供拷贝(移动)构造接口了!所以不会编译报错。又因为没有临时对象,所以就不存在拷贝,当然也就不存在什么copy elision的说法了!非常棒的改进!这也是C++官方网站“Return value optimization is mandatory and no longer considered as copy elision”的含义。

于是

Vector GetValue2()
{
	return Vector();
}

大致会被编译器处理成下面这样

Vector GetValue2(Vector& __result)
{
    __result.Vector::Vector();
	return ;
}

可以明显看到,没有临时变量的生成,也不需要拷贝构造函数!

这意味着,我们以后看到return Vector()这种代码的时候,可以放心地认为,“返回过程没有临时对象的产生”!(不管项目使用的标准是C++17之前的还是之后的,都没有临时对象产生,C++17之前临时对象被copy elision优化掉了,C++17之后则压根就没什么临时对象)。

RVO的情况说了,NRVO呢?

这不太一样,因为NRVO里面一定是有一个中间对象的,就是所谓的named value,如果class没有拷贝构造函数,NRVO就不能工作!

我们刚才说RVO的时候,可以看到中间没有任何临时变量的生成,也没有拷贝操作,所以不需要拷贝构造函数。

下面的代码中,

Vector GetValue1()
{
	Vector a;
	return a;
}
Vector v = GetValue1();

以上代码会被编译器处理成这样

Vector GetValue1(Vector& __result)
{
	//Vector a; 这个会被优化掉
    __result.Vector::Vector() ;//a的构造会直接替换成__result的构造
    //后续a的操作,都会被直接替换为__result
	return ;
}
Vector v = GetValue1();

从C++语法上来说,Vector v = GetValue1();的确需要拷贝构造函数,但编译器实际优化的时候,中间的拷贝会被优化掉,直接操作v。

为了验证这个猜想,我们写出以下代码

class Vector
{
public:
	Vector() = default;
	Vector(const Vector& v2) { cout << "copy" << endl; }

	int x;
	int y;
};
Vector GetValue1()
{
	Vector a;
	a.x = 100;
	a.y = 200;
	return a;
}
int main()
{
	Vector test= GetValue1();  

	cout << test.x << endl;    
    return 0;
}

在Debug模式下,输出如下

 可以看到,拷贝构造函数还是被调用了!并且因为我们没有在拷贝构造函数里面处理成员,x是一个未初始化的值。

在Release模式下,输出如下

copy的输出没有了!并且x也有正确的值了!究其原因,乃在于此时发生了NRVO,函数内部的a被直接替换为了test,test.x = 100,当然会得到正确的X了! 

 

发展到C++17,RVO和NRVO,虽然只有一字之差,但已经是不同的东西了。

RVO:不依赖拷贝构造函数的直接返回。

NRVO:依赖拷贝构造函数的copy elision(优化的时候,中间的拷贝会被优化掉)。

拷贝构造函数中的Copy Elision处理

上面讨论了RVO NRVO的前因后果,NRVO只是copy elision的其中一个应用场景。copy elision还有一个典型的应用场景是构造函数的嵌套。

下面这行代码

	Vector v = Vector(Vector(Vector()));

我们明显可以看出,多层的拷贝构造是不必要的,所以实际上最后也只调用了一个构造函数。和RVO一样,C++17之前,还叫做copy elision,因为它本质就是对于copy对象的消除,所以依赖拷贝构造函数。

C++17之后,它本质上已经不是copy elision了,当然也就不再依赖拷贝构造函数,这种情形的进化和RVO是一个道理。

但为了称呼上的方便,继续叫它copy elision,虽然不准确,但能直观表达出没有中间对象的意图,无伤大雅,知道其中原理就行了!

让我们看下面这个有趣的例子

class Vector
{
public:
	Vector()				 {cout << " default constructor" << endl;}
	Vector(const Vector& Other) = delete;
	Vector(Vector&& Other) = delete;

	friend const Vector operator + (const Vector& v1, const Vector& v2)
	{
		...
	}

};	
Vector a0;
Vector a1;
Vector a2 (a0 + a1);

在C++17中,Vector a2a(a0+a1)竟然能通过,要知道Vector的拷贝构造和移动构造都被标记为delete了,为什么这种看似拷贝构造的行为还能通过呢?原因就在于a0+a1返回的是一个临时变量,是一个右值,这在C++17中被认为是要被直接优化掉的,所以就直接在a2的地址里面构造了a0+a1。但下面这行代码却不行

Vector a2 (a0);

因为a0不是一个右值,不满足条件,所以调用的是拷贝构造,但拷贝构造又被标记为了delete,所以编译报错。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值