天天看点

[转]细述临时对象

  临时对象(Temporary Object)

一、什么情况下可能出现临时对象?

1、传值参数(pass-by-value parameter) 代码:

void func(String str)
		{
			cout << str;
		}
         

当进行这样一个调用时:

代码:

String s;
		func(s);
         

s将被拷贝构造一个备份(临时对象),这个备份参与函数体内的运算,原件不会被改动。

2、返回值(return value)

代码:

String func()
		{
			String s("Hello, world");
			return s;
		}
         

当s被返回时,会被拷贝构造一个备份(临时对象),这个变量被返回,函数体内的局部变量被销毁。

3、隐式类型转换(Implicitly Typecast)

代码:

class String
		{
		public:
			String();
			String(const char *str);
			operator=(const String &rhs);
		};
         

当实行一个赋值动作:

代码:

String s;
			s = "Hello, world";
         

时,因为String:

[转]细述临时对象

perator=并没有针对char *和const char *的重载,而最可能的做法就是将const char *隐式转换为一个String对象。在这个过程中就会产生一个String类的临时对象。

4、++ 和 --

代码:

std::list<int>::iterator iter;
		iter++;
         

因为iter++的返回值需要返回原值,所以这个值在增1之前必须保留,这个保留就必须使用一个临时对象。这相当于是返回值的一个典型情况。

5、值存储的STL容器

主要针对std::vector,因为vector具有自动扩充容量的功能,在每次扩充容量的时候,大部分实现的算法是new一个新的内存块,将原有的每个元素拷贝复制到新的内存,然后删除原有的内存块。这期间要使用大量的临时对象。

二、临时对象的弊端

临时对象实际上就是通过拷贝构造函数产生的,没有变量名的const对象,所以每产生这样一个对象,都会调用一次拷贝构造函数。如果这个类很“大”,那么会占用相当的资源和时间,从而降低效率。特别是在第5种情形中,会产生大量的临时对象,情况尤其严重。

三、临时对象的应对之道

解决临时对象的主要方法就是使用指针和引用,减少构造函数调用的机会。

1、避免使用传值参数

实际上绝大部分的传值参数都可以使用常量引用(const &)来代替,而常量引用的参数不会产生临时对象。

代码:

void func(const String& str)
		{
			cout << str;
		}
         

这样的写法可以有效的避免在参数传递过程中产生临时对象。

但是,引用通常是由指针实现的,对于基本类型来说,指针操作比直接操作要慢一些,并且增加了操作的复杂性。而基本类型的操作都不复杂,而且很快,所以,对于基本类型来说,没有必要使用常量引用手法。

2、隐式类型转换

隐式类型转换的问题是定义了针对某个类型的构造函数,但是没有定义针对该类型的operator=所导致的,所以,构造函数和operator=必须成对出现。

3、++ 和 --

这个问题已经讨论过很多了,

代码:

operator++() 的实现方法是
		{
			return ++obj.value;
		}
		
		operator++(int) 的实现方法是
		{
			Class tmp = obj;
			++obj.value;
			return tmp;
		}
         

因此,在可能的情况下,使用++i代替i++,就可以避免出现临时对象。

4、在STL容器(尤其是vector)中使用对象指针而不是对象实体作为元素

vector因为有重新分配内存的动作,在这个动作中会产生大量的临时对象,极大的降低效率。解决方法就是使用对象指针作为vector的元素,避免采用对象实体。另外,用reserve事先分配内存也是避免内存交换的方法。

代码:

std::vector<String *> str_vector;
         

但是要注意,STL使用的是值(value)语意,所以对容器中指针的管理要自己负责,比如容器析构和copy时,可能要自己多做一些工作,以保证数据的正确性。

更好的方法当然是对指针语意的vector做一个封装。

5、函数的返回值

函数的返回值不能以引用或指针的方式返回函数内部的局部变量,如果需要返回局部变量的值,必须以Object形式返回。在这种情况下,临时对象是不可避免的。这个问题上,《Effective C++》Item 23 给出了更详细的讨论。

对于无法避免的临时对象,我们至少可以将“危害”降到最低点。

利用编译器优化

a) 上面提到,函数必须以值的形式返回局部变量。由于这种形式比较普遍并且无法避免,现代的编译器大部分都可以实行一个叫做 Named Return Value(NRV)的优化。这个优化可以避免不必要的临时对象的产生。在编译器实施NRV优化时,如下的代码:

代码:

X bar()
			{
				X xx;
				//... operate xx
				return xx;
			}
         

将被优化为类似这样:

代码:

void bar( X& __result)
			{
				__result.X::X();
				
				//... operate __result
				
				return;
			}
         

很明显的,这个优化减少了一个临时对象的产生。

NRV优化由编译器自动实施,并没有什么编译选项,也无法人工干预。所以,不能依赖于该优化,不过该优化在某些情况下的确可以提高效率。

对NRV的讨论,可以参考《深度探索C++对象模型》一书的“在编译器层面做优化”一章。

b) 目前几乎所有的编译器都可以保证

代码:

Class c = a + b;
         

这样的形式将不会产生临时对象。

6、其它

尽可能少的调用

这个问题最典型的现象就是循环的终止条件。

代码:

for(std::vector<String *>::iterator iter = str_vector.begin(); iter != str_vector.end(); ++iter)
			{
				//...
			}
         

在这里,每循环一次,iter != str_vector.end() 这个判断就要进行一次,因为str_vector.end()是一个函数调用,因为它的返回值是一个iterator的Object,所以每次调用都会产生一个临时对象。如果写成这样,临时对象就会只被生成一次:

代码:

std::vector<String *>::iterator end_iter = str_vector.end();
			for(std::vector<String *>::iterator iter = str_vector.begin(); iter != end_iter; ++iter)
			{
				//...
			}
         

四、其它问题

临时对象的生命周期

临时对象是不可见的,但是,在特定的情况下,它的生存周期问题可能导致问题。对这个问题,标准规格上的定义是:

临时对象的被摧毁,应该是对完整表达式求值过程中的最后一个步骤。该完整表达式造成临时对象的产生(Section 12.2)

对这个问题的更详细的讨论,可以参见《深度探索C++对象模型》 6.3节,临时性对象。另外,在《C++设计与演化》一书中也有论述。