RVO优化:
return value optimistic,指当一个函数返回一个值类型而非引用类型时,可以绕过拷贝/移动构造函数,直接在调用函数的地方构造返回值。要发生rvo需要有三个条件:
1,如果类型是自定义类型,那么此时的拷贝/移动构造函数是可访问的
2,返回类型必须与实际类型完全一致,即返回时不需要进行隐式类型转换,否则不能进行rvo,因为不知道用户定义的类会不会有特别的类型转换构造函数
3,返回类型必须为值类型,而非引用类型,当然,这也是为什么需要rvo的原因--避免拷贝带来的额外开销
下面的代码是一个rvo的演示
class Foo { public: Foo() { cout << "default constructor " << endl; } Foo(const Foo& f) { cout << "copy constructor " << endl; } Foo(int i) { cout << "convert constructor " << endl; } Foo(Foo &&f) { cout << "move constructor " << endl; } ~Foo() { cout << "destructor " << endl; } }; Foo fun(int i = 9) { Foo f1; return f1; //return std::move(f1); } int main(){ Foo bob = fun(); return 0; }
输出结果是这样的:
default constructor destructor
在执行Foo bob = fun()时, 本意是主动呼叫拷贝构造函数,但是由于Foo定义了移动构造函数,使用普通的函数匹配规则来确定使用哪个构造函数,由于fun()的返回值是右值,所以使用移动构造函数,由于此时移动构造函数是可访问的,编译器可以绕过拷贝/移动构造函数,直接构造对象。所以不会先由fun()返回一个临时对象,再利用这个临时对象构造bob的过程,而是这个构造的临时量就是bob,不拷贝。
另外,函数的执行过程是,先执行函数体,在遇见return时,将return的值拷贝/移动到调用函数的地方,然后开始析构栈中的对象,但是rvo允许直接在调用函数的地方构造应该return的那个对象,所以这一次移动构造函数也被绕过了。
如果我们把这些优化都关掉,会得到这样的结果:
default constructor move constructor destructor move constructor destructor destructor
即先构造f1,然后遇见return,移动到临时对象,析构f1,将临时对象移动到bob,析构临时对象,临时bob
但是如果我们这么写fun(),编译器就不会进行RVO:
Foo fun(int i = 9) { Foo f1, f2; if (i < 0) { return f1; } return f2; }
前面说到,RVO是在构造需要return的对象时,直接在上一级栈帧中构造它(即调用函数的地方),而不是在它自己的栈帧中构造,这样就避免了将该对象拷贝至上级栈帧中的过程,可是修改后的fun,编译器无法在编译器推断出哪个对象会被返回,也就无从进行RVO,因此我们得到了这样的结果
default constructor default constructor move constructor destructor destructor destructor
先构造出两个对象f1,f2,移动f2到函数调用处,析构f1,f2,这里只有一个移动构造函数是因为编译器优化掉了bob的移动构造函数。
总结:
1, 在构造对象时,如果有移动构造函数,使用普通函数匹配规则构造对象,否则正常构造。
2,正常构造函数时,使用'='就是主动呼叫移动构造函数即string s = "999"是让编译器先把"999"转换为string类型,再拷贝/移动,哪怕string类型有一个string(const char *)的构造函数,也要这么干
3,上面两个过程,在移动/拷贝构造函数都可以访问时,如果为移动构造函数,则直接拿着右值来使,不再调用移动构造函数,如果是拷贝构造函数,则直接拿着转换后的临时string来用,不拷贝,也就让string s = "999" 变成了string s("999") (很明显,隐式转换用的就是string(const char *)这个构造函数)
4, RVO优化只有同时满足:返回值是值类型,并且无需类型转换,编译器能够在编译器判断出哪个变量会被返回时,才会发生。发生RVO时,如果某个对象instance1会被返回,则不在该函数的栈帧中构造它,而是在调用函数的地方也就是上级栈帧中构造instance1。
5,函数在执行到return语句时,理论上先讲返回值拷贝/移动置调用的地方,然后开始析构函数栈中的自动对象