以前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”的含义。
这意味着,我们以后看到return Vector()这种代码的时候,可以放心地认为,“返回过程没有临时对象的产生”!(不管项目使用的标准是C++17之前的还是之后的,都没有临时对象产生,C++17之前临时对象被copy elision优化掉了,C++17之后则压根就没什么临时对象)。
RVO的情况说了,NRVO呢?
这不太一样,因为NRVO里面一定是有一个中间对象的,就是所谓的named value,如果class没有拷贝构造函数,NRVO就不能工作!
为什么?为什么RVO可以不需要拷贝构造函数,NRVO就必须要拷贝构造函数?
RVO的情况,我们可以直接把Vector()这样的数据构造到外部的变量地址里面,比如
- Vector GetValue2()
- {
- return Vector();
- }
- Vector v = GetValue2();
最后的return Vector()直接就用Vector()作为参数,在v的地址里构造了对象。所以没有拷贝构造函数也OK!这种情况,只涉及到普通构造函数,不涉及拷贝构造函数。
但是下面的代码中,
- Vector GetValue1()
- {
- Vector a;
- return a;
- }
- Vector v = GetValue1();
GetValue1()返回的是a,即使进行copy elision优化,外界所知的也只是a而已,要用a去给v构造,Vector必须要有拷贝构造函数!
所以NRVO的本质还是copy elision,这里的copy指的是中间copy出的临时对象,不是拷贝构造函数的copy constructor。只不过因为copy elision的存在,我们同样可以为,return a返回的过程中也不会有临时对象(被优化掉了)!
发展到C++17,RVO和NRVO,虽然只有一字之差,但已经是不同的东西了。
RVO:不依赖拷贝构造函数的直接返回。
NRVO:依赖拷贝构造函数的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,所以编译报错。