Leave机制与C++异常机制的对比

原文链接: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的子类,那么代码没问题,对象是分配在堆上,并且被放入了清理栈。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值