• 析构函数为什么不抛异常


    C++ 用异常使得可以将正常执行代码和出错处理区别开来。 比如一个栈,其为空时,调用其一个pop 函数,接下来怎么办? 栈本身并不知道该如何处理,需要通知给其调用者(caller),因为只有调用者清楚接下来该怎么做。 异常,就提供了一个很好机制。 但是异常需要操作系统,编译器,RTTI的特性支持。

    下面围绕一个问题 “为什么析构函数不能抛出异常?” 展开C++中异常的实现。

    Effective C++ 里面有一条”别让异常逃离析构函数“,大意说是Don't do that, otherwise the behavior is undefined. 这里讨论一下从异常的实现角度,讨论一下为什么不要 ?

    1. 函数调用框架和SEH( Structure Error Handling)

       程序

    复制代码
    复制代码
    1 int widget( int a, int b)
    2 {
    3 return a + b;
    4 }
    5
    6  int bar(int a, int b)
    7 {
    8 int c = widget(a, b);
    9 return c;
    10 }
    11
    12  int foo( int a, int b)
    13 {
    14 int c=bar(a, b);
    15 return c;
    16 }
    17
    18  int main()
    19 {
    20 foo( 1, 2);
    21 }
    复制代码
    复制代码

       其汇编代码

    复制代码
    复制代码
    1 PUBLIC ?widget@@YAHHH@Z ; widget
    2 ; COMDAT ?widget@@YAHHH@Z
    3  _TEXT SEGMENT
    4 _a$ = 8; size = 4
    5  _b$ = 12; size = 4
    6  ?widget@@YAHHH@Z PROC ; widget, COMDAT
    7  
    8  ; 4 : {
    9  
    10  push ebp
    11  mov ebp, esp
    12  sub esp, 192; 000000c0H
    13  push ebx
    14  push esi
    15  push edi
    16  lea edi, DWORD PTR [ebp-192]
    17  mov ecx, 48; 00000030H
    18  mov eax, -858993460; ccccccccH
    19  repstosd
    20
    21  ; 5 : return a + b;
    22  
    23  mov eax, DWORD PTR _a$[ebp]
    24  add eax, DWORD PTR _b$[ebp]
    25
    26  ; 6 : }
    27  
    28  pop edi
    29  pop esi
    30  pop ebx
    31  mov esp, ebp
    32  pop ebp
    33  ret0
    34 ?widget@@YAHHH@Z ENDP ; widget
    35  _TEXT ENDS
    36 PUBLIC ?bar@@YAHHH@Z ; bar
    37  EXTRN __RTC_CheckEsp:PROC
    38  ; Function compile flags: /Odtp /RTCsu /ZI
    39 ; COMDAT ?bar@@YAHHH@Z
    40  _TEXT SEGMENT
    41 _c$ = -8; size = 4
    42  _a$ = 8; size = 4
    43  _b$ = 12; size = 4
    44  ?bar@@YAHHH@Z PROC ; bar, COMDAT
    45  
    46  ; 9 : {
    47  
    48  push ebp
    49  mov ebp, esp
    50  sub esp, 204; 000000ccH
    51  push ebx
    52  push esi
    53  push edi
    54  lea edi, DWORD PTR [ebp-204]
    55  mov ecx, 51; 00000033H
    56  mov eax, -858993460; ccccccccH
    57  repstosd
    58
    59  ; 10 : int c = widget(a, b);
    60  
    61  mov eax, DWORD PTR _b$[ebp]
    62  push eax
    63  mov ecx, DWORD PTR _a$[ebp]
    64  push ecx
    65  call ?widget@@YAHHH@Z ; widget
    66  add esp, 8
    67  mov DWORD PTR _c$[ebp], eax
    68
    69 ; 11 : return c;
    70
    71 mov eax, DWORD PTR _c$[ebp]
    72
    73 ; 12 : }
    74
    75 pop edi
    76 pop esi
    77 pop ebx
    78 add esp, 204; 000000ccH
    79 cmp ebp, esp
    80 call __RTC_CheckEsp
    81 mov esp, ebp
    82 pop ebp
    83 ret0
    84 ?bar@@YAHHH@Z ENDP ; bar
    85 _TEXT ENDS
    86 PUBLIC ?foo@@YAHHH@Z ; foo
    87 ; Function compile flags: /Odtp /RTCsu /ZI
    88 ; COMDAT ?foo@@YAHHH@Z
    89 _TEXT SEGMENT
    90 _c$ = -8; size = 4
    91 _a$ = 8; size = 4
    92 _b$ = 12; size = 4
    93 ?foo@@YAHHH@Z PROC ; foo, COMDAT
    94
    95 ; 15 : {
    96
    97 push ebp
    98 mov ebp, esp
    99 sub esp, 204; 000000ccH
    100 push ebx
    101 push esi
    102 push edi
    103 lea edi, DWORD PTR [ebp-204]
    104 mov ecx, 51; 00000033H
    105 mov eax, -858993460; ccccccccH
    106 repstosd
    107
    108 ; 16 : int c=bar(a, b);
    109
    110 mov eax, DWORD PTR _b$[ebp]
    111 push eax
    112 mov ecx, DWORD PTR _a$[ebp]
    113 push ecx
    114 call ?bar@@YAHHH@Z ; bar
    115 add esp, 8
    116 mov DWORD PTR _c$[ebp], eax
    117
    118 ; 17 : return c;
    119
    120 mov eax, DWORD PTR _c$[ebp]
    121
    122 ; 18 : }
    123
    124 pop edi
    125 pop esi
    126 pop ebx
    127 add esp, 204; 000000ccH
    128 cmp ebp, esp
    129 call __RTC_CheckEsp
    130 mov esp, ebp
    131 pop ebp
    132 ret0
    133 ?foo@@YAHHH@Z ENDP ; foo
    134 _TEXT ENDS
    135 PUBLIC _main
    136 ; Function compile flags: /Odtp /RTCsu /ZI
    137 ; COMDAT _main
    138 _TEXT SEGMENT
    139 _main PROC ; COMDAT
    140
    141 ; 21 : {
    142
    143 push ebp
    144 mov ebp, esp
    145 sub esp, 192; 000000c0H
    146 push ebx
    147 push esi
    148 push edi
    149 lea edi, DWORD PTR [ebp-192]
    150 mov ecx, 48; 00000030H
    151 mov eax, -858993460; ccccccccH
    152 repstosd
    153
    154 ; 22 :
    155 ; 23 : foo( 1, 2);
    156
    157 push2
    158 push1
    159 call ?foo@@YAHHH@Z ; foo
    160 add esp, 8
    161
    162 ; 24 : }
    163
    164 xor eax, eax
    165 pop edi
    166 pop esi
    167 pop ebx
    168 add esp, 192; 000000c0H
    169 cmp ebp, esp
    170 call __RTC_CheckEsp
    171 mov esp, ebp
    172 pop ebp
    173 ret0
    174 _main ENDP
    175 _TEXT ENDS
    176 END
    复制代码
    复制代码

       调用框架 

    函数调用框架

     2。  加入SEH 之后,函数调用框架稍微修改一下: 对每一个函数加入一个Exception_Registration 的链表,链表头存放在FS:[0] 里面。当异常抛出时,就去遍历该链表找到合适的catch 块。 对于每一个Exception_Registration 存放链表的上一个节点,异常处理函数( Error Handler). 用来处理异常。 这些结构都是编译器加上的,分别在函数调用的prologue 和epilogue ,注册和注销 一个异常处理节点。

    加入SEH 后的函数调用框架

     NOTE: error handling

    1. 当异常发生时,系统得到控制权,系统从FS:[0]寄存器取到异常处理链的头,以及异常的类型, 调用异常处理函数。(异常函数是编译器生成的)

    2. 从链表头去匹配 异常类型和catch 块接收的类型。( 这里用到RTTI 信息)

    3. unwind stack。这里需要析构已经创建的对象。( 这里需要判断析构哪些对象,这一步是编译器做的)

    4. 执行catch 块代码。

    后返回到程序的正常代码,即catch块下面的第一行代码。

      可见,在exception 找到对应的 Catche 块后, 去栈展开(unwind stack),析构已有的对象后,进入到Catch 块中。 问题是: 程序怎么知道程序运行到哪里? 哪些对象需要调用析构函数? 这也是编译器做的,对于每一个Catch 块,其记录下如果该catch 块若被调用,哪些对象需要被析构。 这有这么一张表。具体实现可以参见reference2.

    3. 当析构抛出异常时,接下来的故事。

       实验1:  Base 类的析构抛出异常;

    复制代码
    1 class Base
    2 {
    3 public:
    4 void fun() { throw1; }
    5 ~Base() { throw2; }
    6 };
    7
    8 int main()
    9 {
    10 try
    11 {
    12 Base base;
    13 //base.fun();
    14 }
    15 catch (...)
    16 {
    17 //cout <<"get the catch"<<endl;
    18 }
    19 }
    复制代码

             运行没有问题。

       实验2: 打开上面注释掉的第13行代码(//base.fun();,再试运行,结果呢? 在debug 模式下弹出对话框

      

    为什么呢?

    因为SEH 是一个链表,链表头地址存在FS:[0] 的寄存器里面。 在实验2,函数base.fun先抛出异常,从FS:[0]开始向上遍历 SHL 节点,匹配到catch 块。 找到代码里面为一个catch块,再去展开栈,调用base 的析构函数,然而析构又抛出异常。 如果系统再去从SEL链表匹配,会改变FS:[0]值,这时候程序迷失了,不知道下面该怎么什么? 因为他已经丢掉了上一次异常链那个节点。


    实验3:如果析构函数的异常被处理呢, 程序还会正常运行吗?

    复制代码
    1 class Base
    2 {
    3 public:
    4 void fun() { throw1; }
    5 ~Base()
    6 {
    7 try
    8 {
    9 throw2;
    10 }
    11 catch (int e)
    12 {
    13 // do something
    14 }
    15 }
    16 };
    17
    18 int main()
    19 {
    20 try
    21 {
    22 Base base;
    23 //base.fun();
    24 }
    25 catch (...)
    26 {
    27 //cout <<"get the catch"<<endl;
    28 }
    29 }
    复制代码

    的确可以运行。

    因为析构抛出来的异常,在到达上一层析构节点之前已经被别的catch 块给处理掉。那么当回到上一层异常函数时, 其SEH 没有变,程序可以继续执行。

    这也许就是为什么C++不支持异常中抛的异常。

    4. 效率:

        当无异常抛出时,其开销就是在函数调用的时候注册/注销 异常处理函数,这些开销很小。

        但是当异常抛出时,其开销就大了,编译异常链,用RTTI比配类型,调用析构;但是比传统的那种返回值,层层返回,效率也不会太差。 带来好的好处是代码好维护,减少出错处理的重复代码,并且与逻辑代码分开。

        权衡一下,好处还是大大的:)

    5. 总结一下流程:

      为了安全,”析构函数尽可能的不要抛出异常“。

      如果非抛不可,语言也提供了方法,就是自己的异常,自己给吃掉。但是这种方法不提倡,我们提倡有错早点报出来。

    Note:

    1.同样还有一个问题,”构造函数可以抛出异常么? 为什么?“

    C++ 里面当构造函数抛出异常时,其会调用构造函数里面已经创建对象的析构函数,但是对以自己的析构函数没有调用,就可能产生内存泄漏,比如自己new 出来的内存没有释放。

    有两个办法。在Catch 块里面释放已经申请的资源 或者 用智能指针把资源当做对象处理。

    Delphi 里面当构造函数抛异常时,在其执行Catch 代码前,其先调用析构函数。

    所以,构造抛出异常,是否调用析构函数,不是取决于技术,而是取决于语言的设计者。

    2. 关于多线程,异常是线程安全的。 对于每一个线程都有自己的 Thread Info/Environment Block. 维护自己的SEH结构。

    Reference:

    1.http://www.codeproject.com/KB/cpp/exceptionhandler.aspx

    2.http://baiy.cn/doc/cpp/inside_exception.htm

    3.http://www.mzwu.com/article.asp?id=1469

  • 相关阅读:
    自定义注解标签验证
    redis-服务器配置-主从
    POJ-2195 Going Home(最小费用最大流模板)
    POJ-1087 A Plug for UNIX
    HDU-3625 Examining the Rooms (第一类斯特林数)
    网络流入门
    CodeForces-1082G Increasing Frequency
    python学习之模块-模块(三)
    python学习之模块-模块(二)
    python学习之模块-模块(一)
  • 原文地址:https://www.cnblogs.com/sideny/p/3265825.html
Copyright © 2020-2023  润新知