C++中临时对象及返回值优化
2017-05-22 15:41
295 查看
一、什么是临时对象?
C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象.通常出现在以下两种情况:
1 、为了使函数调用成功而进行隐式类型转换的时候。
传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁。如下例://计算字符ch在字符串str中出现的次数 int countChar (const string& str, char ch); char buffer[]; char c; //调用上面的函数 countChar (buffer, c);1
2
3
4
5
6
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
5
6
[/code]
我们看的第一个参数为char[],而函数的参数类型为const string&,参数不一致,看看能否进行隐式转化,string类有个构造函数是可以作为隐式转化函数的。那么编译器会产生一个 string的临时变量,以buffer为参数进行构造,那么countChar中的str参数会绑定到此临时变量上,直到函数返回时销毁。
注意这样的转化只会出现在两种情况下:函数参数以传值(by value)的方式传递 或者 对象被传递到一个 reference-to-const 参数上。
传值方式:
int countChar (string str, char ch); string buffer; char c; //参数通过传值方式传递 countChar (buffer, c);1
2
3
4
5
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
5
[/code]
这种方法会调用string的拷贝构造函数生成一个临时变量,再将这个临时变量绑定到str上,函数返回时进行销毁。
传常量引用:
开始的实例即时属于这种情况,但一定强调的是传递的是const型引用,如将开始函数的原型改为int countChar (string& str, char ch);1
![](http://static.blog.csdn.net/images/save_snippets_01.png)
![](http://static.blog.csdn.net/images/save_snippets_01.png)
1
[/code]
下面调用相同,编译器会报错!为什么C++设计时要求当对象传递给一个reference-to-non-const 参数不会发生隐式类型转化呢?
下面的实例可能向你说明这样设计的目的:
//声明一个将str中字符全部转化为大写 void toUpper (string& str); char buffer[] = "hazirguo"; toUpper(buffer); //error!!非const引用传递参数不能完成隐式转化1
2
3
4
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
[/code]
如果编译器允许上面的传递完成,那么,会生成一个临时对象,toUpper函数将临时变量的字符转化为大写,返回是销毁对象,但是对buffer内容毫无影响!程序设计的目地是期望对“非临时对象”进行修改,而如果对reference-to-non-cosnt对象进行转化,函数只会对临时变量进行修 改。这就是为什么C++中要禁止non-const-reference参数产生临时变量的原因了。
2、当函数返回对象的时候。
当函数返回一个对象时,编译器会生成一个临时对象返回,如声明一个函数用来合并两个字符串:const string strMerge (const string s1, const string s2);1
![](http://static.blog.csdn.net/images/save_snippets_01.png)
![](http://static.blog.csdn.net/images/save_snippets_01.png)
1
[/code]
大多时候是无法避免这样的临时变量产生的,但是现代编译器可以将这样的临时变量进行优化掉,这样的优化策略中,有个所谓的“返回值优化”,下一篇具体讲解。
总结:
临时对象有构造和析构的成本,影响程序的效率,因此尽可能地消除它们。而更为重要的是很快地发现什么地方会生成临时对象:
当我们看到一个reference-to-const参数时,极可能一个临时对象绑定到该参数上;
当我们看到函数返回一个对象时,就会产生临时对象。
参考:http://www.cnblogs.com/hazir/archive/2012/04/18/2456144.html
二、C++中的返回值优化(return value optimization)
返回值优化(Return Value Optimization,简称RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消 耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,下面是 在Visual Studio 2008的Debug模式下做的一个测试:(在GCC下测试的时候可能编译器自己进行了RVO优化,看不到两种代码的区别)#include <iostream> using namespace std; class Rational { public: Rational(int numerator = 0, int denominator = 1) : n(numerator), d(denominator) { cout << "Constructor Called..." << endl; } ~Rational() { cout << "Destructor Called..." << endl; } Rational(const Rational& rhs) { this->d = rhs.d; this->n = rhs.n; cout << "Copy Constructor Called..." << endl; } int numerator() const { return n; } int denominator() const { return d; } private: int n, d; }; const Rational operator*(const Rational& lhs, const Rational& rhs) { cout << "----------- Enter operator* -----------" << endl; Rational tmp(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); cout << "----------- Leave operator* -----------" << endl; return tmp; } int main(int argc, char **argv) { Rational x(1, 5), y(2, 9); Rational z = x * y; cout << "calc result: " << z.numerator() << "/" << z.denominator() << endl; return 0; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[/code]
函数输出截图如下:
可以看到消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。
而如果把operator*换成另一种形式:
const Rational operator*(const Rational& lhs,const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }1
2
3
4
5
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
5
[/code]
就只会消耗一个构造函数的成本了:
参考:http://www.programlife.net/cpp-return-value-optimization.html
三、返回值优化(RVO)与具命返回值优化(NRVO)
这是一项编译器做的优化,已经是一种很常见的优化手段了,搜一下可以找到很多的资料,在MSDN 里也有相关的说明。返回值优化,顾名思义,就是与返回值有关的优化,是当函数是按值返回(而不是引用、指针)时,为了避免产生不必要的临时对象以及值拷贝而进行的优化。
先看看下面的代码:
typedef unsigned int UINT32; class MyCla { public: MyCla(UINT32 a_size = 10):size(a_size) { p = new UINT32[size]; } MyCla(MyCla const & a_right):size(a_right.size) { p = new UINT32[size]; memcpy(p, a_right.p, size*sizeof(UINT32)); } MyCla const& operator = (MyCla const & a_right) { size = a_right.size; p = new UINT32[size]; memcpy(p, a_right.p, size*sizeof(UINT32)); return *this; } ~MyCla() { delete [] p; } private: UINT32 *p; UINT32 size; }; MyCla TestFun() { return MyCla(); } int _tmain(int argc, _TCHAR* argv[]) { MyCla a = TestFun(); return 0; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[/code]
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构造完了之后生命也就终结了。既然这两个临时的对象对于程序 员来说根本就“看不到、摸不着”(匿名对象),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的TestFun 函数中增加一个参数 MyCla&,然后把 a 的地址传进去(注意,这个时候 a 的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用a来代替原来的“匿名对象”,在函数体内部就完 成a的构造。这样,就省下了两个临时变量的开销。这就是所谓的“返回值优化”!在 VC7 里,按值返回匿名对象时,默认都是这么做。
上面说的是“返回值优化(RVO)”,还有一种“具名返回值优化(NRVO)”,是对于按值返回“具名对象”(就是有名字的变量!)时的优化手段,其实道理 是一样的,但由于返回的值是具名变量,情况会复杂很多,所以,能执行优化的条件更苛刻,在下面三种情况下(来自 MSDN),NRVO 将一定不起作用:
不同的返回路径上返回不同名的对象(比如if XXX 的时候返回x,else的时候返回y)
引入 EH 状态的多个返回路径(就算所有的路径上返回的都是同一个具名对象)
在内联asm语句中引用了返回的对象名。
不过就算 NRVO 不能进行,在上面的描述中的 t2 这个临时变量也不会产生,对于 VC 的 C++ 编译器来说,只要你写的程序是把对象按值返回的,它会有两种做法,来避免 t2 的产生。拿下面这个程序来说明:
MyCla TestFun2() { MyCla x(3); return x; }1
2
3
4
![](http://static.blog.csdn.net/images/save_snippets.png)
![](http://static.blog.csdn.net/images/save_snippets.png)
1
2
3
4
[/code]
一种做法是像 RVO一样,把作为表达式中获取返回值来进行构造的变量 a 当成一个引用参数传入函数中,然后在返回语句之前,用要返回的那个变量来拷贝构造 a,然后再把这个变量析构,函数返回原调用点,a 就构造好了。
还有一种方式, 是在函数返回的时候,不析构x,而直接把x的地址放到 exa 寄存器中,返回调到 TestFun2 的调用点上,这时,a 可以用 exa 中存着的地址来进行构造,a 构造完成之后,再析构原来的变量 x !是的,注意到其实这时,x 的生存域已经超出了TestFun2,但由于这里x所在TestFun2的栈虽然已经无效,但是并没有谁去擦写这块存,所以x其实还是有效的,当然,一切 都在汇编的层面,对于C++语言层面来讲是透明的。
参考:http://www.cnblogs.com/liyiwen/archive/2009/12/02/1615711.html
(function () {('pre.prettyprint code').each(function () {
var lines = (this).text().split(′\n′).length;varnumbering = $('').addClass('pre-numbering').hide();
(this).addClass(′has−numbering′).parent().append(numbering);
for (i = 1; i
相关文章推荐
- C++中临时对象及返回值优化
- [转] C++中临时对象及返回值优化
- more effective c++(临时对象的返回值优化)
- C++中临时对象及返回值优化
- C++中临时对象及返回值优化
- C++中临时对象及返回值优化
- 转:C++中临时对象及返回值优化
- 关于C++中的临时对象问题
- C++中的临时对象 zz
- [More Effective C++]条款十九:理解临时对象的来源
- C++中的临时对象
- 【转】c++临时对象与const
- C++中的临时对象
- C++中的临时对象
- C++中的临时对象
- 【翠字营原创】: C++ 临时对象分析小结 涉及函数参数值传递、函数返回值、仿函数
- c++中的临时对象
- c++临时对象
- 浅析C++临时对象的产生相关问题
- C++中的临时对象(http://blog.chinaunix.net/u/11680/showart_316503.html)