原文链接:http://developer.symbian.org/wiki/index.php/A_Comparison_of_Leaves_and_Exceptions
在Symbian系统的早期版本,当时C++还没有异常机制,于是Symbian开发了自己的Leave和TRAP系统。到了Symbian OS v9,异常机制被添加了进来,Leave/TRAP架构也被重新实现。
这篇文档讨论了用C++异常机制重新实现Leave和TRAP的动因,同时也描述了一些相关的技术问题和由此带来的使用限制。
索引
1. 动因
2. 实现细节
3. Leave和TRAP的用法
4. 使用标准的C++异常
5. 最佳实践
6. 可移植性细节
7. FAQs
动因
用C++异常机制来实现TRAP,有以下几点原因:
- 旧式的Symbian TRAP即使在代码没有leave的情况下,依然会耗费CPU资源;而异常机制则显著的节省CPU。
- TRAP与标准C++异常不兼容。这意味着标准C++代码和Symbian风格的C++代码无法共存。而如果用异常机制来实现TRAP,则两者共存于一个程序变为可能(需要小心的编码)。
- C++异常是工业标准,Symbian像它靠拢是符合逻辑的。
- C++异常机制已经标准化,有着更广泛更好的硬件本地支持(例如EABI)。这使得C++异常比TRAP更快,移植起来也更方便。
实现细节
根据Symbian系统的要求,异常机制必须保证在内存不足的情况(Out of Memory,OOM)下,其行为是确定且安全的。这就导致了一个很大的限制(相对于标准异常机制):嵌套异常是不允许的 。
当一个异常对象被抛出的时候,系统必须分配内存来生成这个对象。Symbian系统的实现预留了足够的内存来保证这个对象一定能够创建成功(预留内存是为了应付OOM)。但是,如果是嵌套异常的情况,系统预留的内存就可能不足,而如果这时已经处于OOM状态,则系统就没有更多的可用内存来创建异常对象。
在ARM设备上,如果遇到嵌套异常,Symbian直接调用abort()。这种情况最常发生在栈展开时(即已经发生了一次异常),某个析构函数中又抛出了异常。
注意: 嵌套异常在WINS上是允许的,因为WINS的异常机制是基于WIN32的结构化异常处理(SEH)实现的。因此,在模拟器上运行正常的代码可能在真机上就会不正常。
Leave 和 TRAP 的用法
Symbian OS v9的User::Leave()实现步骤如下:
1. 调用User::Leave()
2. Symbian的“清理栈”展开(The cleanup stack is unwound)
3. 创建一个XLeaveException异常对象并抛出。如果堆上有足够的空间,则这个异常对象的内存分配在堆上;否则,用系统预留的栈内存创建该对象。这段预留的内存保证了在OOM状态下,异常对象依然能够创建成功。
4. 正常的栈展开过程(也就是标准C++异常处理中的栈展开步骤)
5. catch块被执行
这里有几点需要说明:
第2步,销毁清理栈中所有分配在堆上(heap-based)的对象
在这一步,与Leave相关的异常还没有发生(因为只是在做清理栈展开,并没有生成异常对象)。在此过程中抛出异常并没有危险,因为不可能出现异常嵌套的情况,在后面一步的异常对象生成之前,在清理栈上的对象析构函数中产生的异常就会被处理掉(这需要在析构函数中捕获这个异常)。
第4步,销毁在栈上分配(stack based)的对象
在这一步,与Leave相关的异常对象已经创建。在此过程中抛出异常,需要系统支持异常嵌套,因为现在已经(在第3步创建的)有了一个异常对象并且已经抛出,这在WINS上是允许的,但在ARM上却明确禁止。
第5步,执行异常恢复代码(catch块)
在这一步,异常处理已经结束,catch块中只是普通代码,在此过程中抛出异常是安全的。
因此,对于堆上分配的对象(由清理栈负责销毁),在他们的析构函数中使用TRAP是安全的;但对于栈上分配的对象则不是。而CBase类的子类都是在堆上分配内存,所以在CBase的子类析构函数中使用TRAP是安全的。任何在标准的栈展开过程中(上述的第4步)析构的对象,如果其析构函数使用了TRAP或Leave,都是不安全的。因为在第4步,不可能安全的抛出异常。
使用标准的 C++ 异常
我们可以不用Leave/TRAP框架而是直接使用标准的C++异常处理。在这种情况下,限制条件变得很明了。
清理栈是Symbian特有的机制,标准C++中并没有使用。因此在只使用标准C++时,可以只需考虑栈上分配的对象。这里的限制条件同样是:在栈上分配的对象,在其析构函数中使用异常是不安全的。
最佳实践
按照前文做描述的,在CBase的子类析构函数中可以使用TRAP。然而,我们建议不论是堆上分配的对象还是栈上分配的对象,尽可能让其析构函数保持简洁,避免使用leave相关的代码。在析构函数中使用leave机制,暗示着析构过程中的某一步可能会失败,由此可能引发潜在的内存或句柄泄漏。比较明智的做法是,在析构函数中调用的函数应避免使用leave机制(即不要成为L结尾的函数),而应该简单的返回TInt。
一个避免在析构函数中使用L结尾函数的方法是使用“二阶段析构函数”,比如可以在删除对象之前,先调用类似ShutdownL()这样的方法,将所有可能在析构时Leave的函数放在ShutdownL()中。
如果你认为上面的代码会带来额外的复杂性,则可以使用“guards”:使用一个内部变量存储对象的状态,然后在析构函数中用ASSERT检查这个状态,这样用很简单的办法就可以保证在运行时检测出使用错误(usage-errors)。考虑下面的例子:
如果必须在析构函数中调用L结尾的函数,则必须用TRAP处理该函数,否则析构函数中的leave将会导致整个程序终止。
可移植性细节
用C++的异常机制来实现Leave和TRAP,在使用中带来了一些额外的限制。这些限制只影响析构函数,具体规则如下:
- 在栈展开过程中被销毁的类对象,其析构函数中一定不能 使用Leave和TRAP。
- 被清理栈销毁的类对象(例如CBase子类),其析构函数中可以使用 Leave和TRAP。当然,并不推荐这么做。
- 析构函数一定不能 leave或抛出异常,析构函数中的leave必需用TRAP处理,析构函数中的异常必须被捕获。
这些规则应用于析构函数和在析构函数内调用的函数。
FAQs
Q:也就是说,如果类对象分配在堆上,那么在这个类的析构函数中可以使用TRAP ,清理工作是由清理栈来完成的?
A:是的。
Q:那是不是不能直接使用delete 呢?
A:不总是,大多数情况下直接用delete是没问题的。唯一危险的情况是,在栈展开的情况下delete了对象,而该对象的析构函数中使用了TRAP。这是因为此时已经有一个活动的异常对象,TRAP可能会导致后续异常出现(也就是出现了嵌套的异常)。
考虑下面三个例子:
1 在析构函数外调用delete总是安全的。
2 在一个堆上分配的对象的析构函数中调用delete,有时是安全的。
只有当CBar的销毁过程是由清理栈来完成的时候,这段代码才是安全的。因为使用清理栈来销毁对象的过程中,不会抛出异常。
3 在一个栈上分配的对象(例如T类)的析构函数中调用delete,是不安全的。
在这个例子中,TYetAnother被设计用来自动删除堆上分配的对象iFoo。然而,这在任何一个版本的Symbian系统中都是不安全的。
在异常机制被引入用来实现Leave之前,在栈上创建的对象只是被简单的释放内存,并不会调用析构函数。所以iFoo不会被删除。
在Leave用异常来实现之后,对于栈上创建的对象,在栈展开过程中会调用其析构函数。但有可能在delete之前,已经有一个异常对象被抛出,从而导致嵌套异常。
如果你希望实现与上面类似的功能,请使用清理栈。
Q:根据“可移植性细节”中的三条规则,下面的代码是安全的,但不推荐使用?
A:是的,如果CFoo是CBase的子类,那么代码没问题,对象是分配在堆上,并且被放入了清理栈。