Exception Safety issue
问题描述
Evaluation Orders and Disorders
// Example 1(a)//f( expr1, expr2 );// Example 1(b)//f( g( expr1 ), h( expr2 ) );
编译器只要满足下面的规则,为了便于优化执行顺序,而C++标准并不要求具体指令顺序:
- expr1在g执行之前完成,expr2在h执行之前完成
- g, h在f执行之前完成
首先,在函数正式执行之前,所有的Expression Evaluation要完成。
其次,有可能出现乱序。
所以有可能出现下面的执行顺序:
顺序一
- expr1
- expr2
- g
- h
顺序二
- expr1
- g
- expr2
- h
上面两种情况都符合。
Exception Safety Problems
当C++编译器调用new操作符的时候会做两件事:1. 开辟内存,2. 调用类构造函数。所以下面的code如果处理得当,是不会有内存泄漏:
T1* t1 = new T1;T2* t2 = new T2;
当T1构造函数出现异常时,相应的内存会被释放掉。当T2构造函数出现异常时,T2的内存会被释放,T1的内存也有机会被释放。(解决方法是在两条语句之间加上异常处理,当然用smart pointer也可以解决问题。后面会阐述smart pointer的方法)
但是在遇到Expression Evaluation的时候,情况会变得复杂:
// In some header file:void f( T1*, T2* );// In some implementation file:f( new T1, new T2 );
- T2构造函数异常时,根本没有机会释放T1的内存。用异常处理也没办法,因为T1的内存开辟出来赋值给了一个临时变量。而这个变量的有效期就只在那个括号里。
- 乱序让问题更加复杂,因为你不知道是T1先构造还是T2先构造,你不知道是下面的哪种顺序,但是每一种顺序都有问题。所以无从下手。
- 顺序一
- 1: allocate memory for T1
2: construct T1
3: allocate memory for T2
4: construct T2
5: call f()
- 1: allocate memory for T1
- 顺序二
- 1: allocate memory for T1
2: allocate memory for T2
3: construct T1
4: construct T2
5: call f()
- 1: allocate memory for T1
- 顺序一
解决方案
看起来像的解决方案
用auto_ptr
// In some header file:void f( auto_ptr<T1>, auto_ptr<T2> );// In some implementation file:f( auto_ptr<T1>( new T1 ), auto_ptr<T2>( new T2 ) );
看起来用了smart pointer,应该不存在释放内存的问题了。但其实并没有改善,因为乱序问题,你没法保证auto_ptr<T1>
在new T2
之前调用。如果T2构造失败了,new T1
的内存就泄露了。Vice versa。
用参数默认值
// In some header file:void f( auto_ptr<T1> = auto_ptr<T1>( new T1 ), auto_ptr<T2> = auto_ptr<T1>( new T2 ) );// In some implementation file:f();
没有本质的改善。因为虽然参数默认值是在函数声明中,但是那些new
和auto_ptr<>
是在执行时才会去做的事情。一样会有乱序。
真正的解决方案
// In some header file (same as in Example 2b):void f( auto_ptr<T1>, auto_ptr<T2> );// In some implementation file:f( auto_ptr_new<T1>(), auto_ptr_new<T2>() );
看起来和之前的假解决方案很像对不对,区别是相应的new
和auto_ptr<>
构造函数放到一起了。这样不管是auto_ptr_new<T1>()
在前,还是auto_ptr_new<T2>()
在前,都不会引起Exception Safety问题。
这个方案也就是网上经常有人问到的“为什么要用make_xxx_ptr,而不用new xxx_ptr?”的原因之一,xxx_ptr可以是任何的smart pointer(包括但不限于:auto_ptr, shared_ptr, unique_ptr):
- 可以避免Exception Safety问题
- 避免显示使用
new
运算符,避免潜在问题- 局限是只能用默认构造函数,而无法使用带参数构造函数
下面也是一个办法:
// In some header file:void f( auto_ptr<T1>, auto_ptr<T2> );// In some implementation file:{ auto_ptr<T1> t1( new T1 ); f( t1, auto_ptr<T2>( new T2 ) );}
这个方法更好:
// In some header file:void f( auto_ptr<T1>, auto_ptr<T2> );// In some implementation file:{ auto_ptr<T1> t1( new T1 ); auto_ptr<T2> t2( new T2 ); f( t1, t2 );}
总结
以Exception-Safe Function Calls作者的guide lines作总结:
Perform every resource allocation (e.g., new) in its own code statement which immediately gives the new resource to a manager object (e.g., auto_ptr).
意思是一行就只分配一个资源,并且立即将该资源交给smart pointer管理。
参考文献
Exception-Safe Function Calls
GotW #89 Solution: Smart Pointer