临时变量带给你的困惑
返回值优化(RVO),顾名思义,就是与返回值有关的优化,是当函数是按值返回(而不是引用啊、指针)时,为了避免产生不必要的临时对象以及值拷贝而进行的优化。
先看看下面的代码:
class MyCla
{
public:
??? MyCla(int a_size = 10):size(a_size) {
??????? p = new int[size];???????
??? }
??? MyCla(MyCla const & a_right):size(a_right.size) {
??????? p = new int[size];
??????? memcpy(p, a_right.p, size*sizeof(int));
??? }
??? MyCla const& operator = (MyCla const & a_right) {
??????? size = a_right.size;
??????? p = new int[size];
??????? memcpy(p, a_right.p, size*sizeof(int));
??????? return *this;
??? }
??? ~MyCla() {
??????? delete [] p;
??? }
private:
??? int *p;
??? int size;
};
MyCla TestFun() {
??? return MyCla();
}
int _tmain(int argc, _TCHAR* argv[])
{
??? MyCla a = TestFun();
??? return 0;
}
TestFun() 函数返回了一个 MyCla 对象,而且是按值传递的。
在没有任何“优化”之前,这段代码的行为也许是这样的:return MyCla() 这行代码中,构造了一个 MyCla 类的临时的无名对象(姑且叫它t1),接着把 t1 拷贝到另一块临时对象 t2(不在栈上),然后函数保存好 t2 的地址(放在 eax 寄存器中)后返回,TestFun 的栈区间被“撤消”(这时 t1 也就“没有”了,t1 的生存域在 TestFun 中,所以被析构了),在 MyCla a = TestFun(); 这一句中,a 利用 t2 的地址,可以找到 t2 进行,接着进行构造。这样 a 的构造过程就完成了。然后再把 t2 也“干掉”。
可以看到,在这个过程中,t1 和 t2 这两个临时的对象的存在实在是很浪费的,占用空间不说,关键是他们都只是为a的构造而存在,a构造完了之后生命也就终结了。既然这两个临时的对象对于程序员来说根本就“看不到、摸不着”(匿名对象嘛,你怎么引用?),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的fun函数中增加一个参数 A&,然后把 a 的地址传进去(注意,这个时候 a 的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用 a 来代替原来的“匿名对象”,在函数体内部就完成 a 的构造。这样,就省下了两个临时变量的开销。这就是所谓的“返回值优化”~!在 VC7 里,按值返回匿名对象时,默认都是这么做。
上面说的是“返回值优化(RVO)”,还有一种“具名返回值优化(NRVO)”,是对于按值返回“具名对象”(就是有名字的变量!)时的优化手段,其实道理是一样的,但由于返回的值是具名变量,情况会复杂很多,所以,能执行优化的条件更苛刻,在下面三种情况下(来自MSDN),NRVO 将一定不起作用:
1.??????? 不同的返回路径上返回不同名的对象(比如if XXX 的时候返回x,else的时候返回y)
2.??????? 引入 EH(exception handling) 状态的多个返回路径(就算所有的路径上返回的都是同一个具名对象)
3.??????? 在内联asm语句中引用了返回的对象名。
不过就算 NRVO 不能进行,在上面的描述中的 t2 这个临时变量也不会产生,对于VC的 C++编译器来说,只要你写的程序是把对象按值返回的,它会有两种做法,来避免 t2 的产生。拿下面这个程序来说明:
MyCla TestFun2() {
??? MyCla x(3);
??? return x;
}
一种做法是像 RVO一样,把作为表达式中获取返回值来进行构造的变量 a 当成一个引用参数传入函数中,然后在返回语句之前,用要返回的那个变量来拷贝构造 a,然后再把这个变量析构,函数返回原调用点,a 就构造好了。
还有一种方式,是在函数返回的时候,不析构 x ,而直接把 x 的地址放到 exa 寄存器中,返回调到 TestFun2 的调用点上,这时,a 可以用 exa 中存着的地址来进行构造,a 构造完成之后,再析构原来的变量 x !是的,注意到其实这时,x 的生存域已经超出了 TestFun2,但由于这里 x 所在 TestFun2 的栈虽然已经无效,但是并没有谁去擦写这块存,所以 x 其实还是有效的,当然,一切都在汇编的层面,对于 C++ 语言层面来讲是透明的。