IEEE754-2008 标准详解(六):异常处理
本文为原创文章,转载请注明出处,并注明转载自“黄邦勇帅(原名:黄勇)”
本文是对《C++语法详解》一书相关章节的增补,以增强读者对浮点数的理解,《C++语法详解》网盘地址:https://pan.baidu.com/s/1dIxLMN5b91zpJN2sZv1MNg
本文摘自本人所作《IEEE754-2008标准详解》网盘地址
链接:https://pan.baidu.com/s/10soDctgCJ84MDs3PyhJBcw?pwd=lzku
提取码:lzku
有兴趣的读者可参阅本人所著《C++语法详解》一书,电子工业出版社出版,该书语法示例短小精悍,对查阅C++知识点相当方便,并对语法原理进行了透彻、深入详细的讲解,可确保读者彻底弄懂C++的原理,彻底解惑C++,使其知其然更知其所以然。此书是一本全面了解C++不可多得的案头必备图书。
由于本人能力有限,文中难免有错漏之处,望广大读者指出更正,不胜感激
6.1 基本概念及基本规则
需要弄清楚异常、异常处理、异常处理属性三者的关系,以及处理异常的各种方式。
6.1.1 异常与异常处理
1、异常
- 异常是一种错误(类似于bug),也就是说,在程序中出现了异常,就意味着发生了错误。当然,某些情况下,只要异常不是很严重,程序仍可继续执行。
- IEEE754规定了5种异常:无效操作异常、下溢异常、除零异常、溢出异常、不精确的异常。
- 需要注意的是:某些异常IEEE754并未指定标准的名称。标准名称可直接在编写程序时使用,类似于C++把整型命名为int一样,这样int就是整型的标准名称,在编写程序时就可以使用int表示整型了,当然,你也可以不把整型命名为int,而使用其他名称。
2、异常处理
- 也就是处理异常的地方,如函数、程序块等,分别称为处理函数、处理程序块,在编程语言中通常使用函数来处理异常。
- 处理异常的主要目的是让程序能继续执行并结束运行(当然是不正常的执行并结束),以防止让程序陷入崩溃、死循环而无法结束等局面
3、对“处理”一词的说明
“处理”即可作为名词,也可作为动词。若把“异常处理”中的处理理解为名词,则可将其理解为“异常处理函数或异常处理程序块”的简称,若理解为动词,则意思是“对异常进行处理”。读者应根据上下文进行区分,本文尽量避免这种产生混淆的用法,其他的形式如,
异常处理方式 = 处理异常的方式
进行异常处理 = 对异常进行处理
6.1.2 处理异常的方式
1、有多种异常处理的方式,如,默认处理、即时处理、延迟处理、即时转移控制、延迟转移控制、可恢复模式、不可恢复模式等。这些异常处理的方式在IEEE754中都使用相应的属性来描述。
2、可恢复模式与不可恢复模式
- 不可恢复模式是指,一旦产生了错误,则假设该错误是无可挽回的,程序不能再返回到产生错误的地方继续执行。C++语言的异常处理方式就属于此模式。在IEEE754中,即时或延迟备用异常处理是属于不可恢复模式。
- 可恢复模式是指,一旦产生错误,则假设该错误是可修正的,然后程序在产生错误的地方继续执行。在IEEE754中,默认异常处理和恢复备用异常处理都是属于可恢复模式
3、即时处理与延迟处理
- 即时处理是指,当产生错误时,立即进行处理。
- 延迟处理是指,当产生错误时,稍后处理,即,让程序继续执行并完成当前任务块,再进行异常处理
4、转移控制
转移控制是指的把异常处理转移(throw)到本地之外的其他地方处理异常,或者直接跳过(break或goto)而不处理异常。若异常被转移到其他地方处理,则该异常是怎样被处理的,并不能确定,即,可能已被处理,也可能未被处理,可能继续使用备用异常处理或默认异常处理,也可能不是。
5、默认处理
按IEEE754标准所述,默认异常处理的方式很简单,就是返回一个默认结果,设置一下状态标志,然后就可以继续执行了,这里返回的结果可以简单的理解为向用户返回一个错误代码或输出一串说明性文字,当然也可能什么都不做,可见,这种处理方式是属于可恢复模式和即时处理方式。理论上而言,异常的默认处理方式应该由系统完成(如用户使用的编译器)
6、备用异常处理
- 备用异常处理是相对于默认异常处理而言的,即,不能默认处理时的一种备用处理方案。因此,理论上来讲,备用异常处理应该由用户来完成(即,系统解决不了,需用户自已解决)
- 恢复备用异常处理通常采用的是默认异常处理方式,但与默认异常处理有一些不同,如raiseNoFlag类型的恢复备用异常处理就不需要设置状态标志,注:恢复备用异常处理还有其他多种处理方式,这些方式都与默认异常处理有某些不同。
7、属性、异常处理属性
- 可以使用属性来描述某个对象的某些特性,如,“人”具有“身高”、“体重”、“年龄”等属性,IEEE754也使用属性来描述某些特性。
- IEEE754总共包含以下几大类属性:
- 舍入属性,主要用于四舍五入
- 备用异常处理属性,这是本章需要讲解的属性
- preferredWidth(首选宽度)属性,主要用于表达式求值
- value-changing optimization(值改变优化)属性,主要用于表达式求值
- reproducibility(重现性)属性,主要用于可移植性问题。
- 属性通常含有属性值,属性值可以是常量值,也可以是动态模式的值。
- IEEE754使用异常处理属性来描述异常处理的各种类型,异常处理属性是用于描述异常处理类型、方案、方式的,不同的属性关联不同的异常处理方式。
8、目前IEEE规定了以下几类异常处理方式:
- 默认异常处理:其处理方法为,返回一个结果,继续执行,并设置状态标志。
- 备用异常处理,又分为以下几类
- 即时备用异常处理:立即转到处理程序块处理异常,详见正文。
- 延迟备用异常处理:先按默认方式处理完相关块,再转到处理程序块处理异常,详见正文。
- 恢复备用异常处理:又分为default、raiseNoFlag 、mayRaiseFlag 、recordException、substitute(x) 、substituteXor(x)、abruptUnderflow等7大类,详见正文。
9、本文的名称约定
对于所有的异常处理方式,本文有时会省略异常或备用二字,如
- 备用异常处理 = 备用处理 (不含恢复备用异常处理)
本文将即时备用异常处理和延迟备用异常处理统称为备用异常处理
- 默认异常处理 = 默认处理
注意:默认处理不能是备用的,也就是说,没有默认备用异常处理这一说法
- 延迟备用异常处理 = 延迟处理、延迟备用处理、延迟异常处理
- 即时备用异常处理 = 即时处理、即时备用处理、即时异常处理
- 恢复备用异常处理 = 恢复处理、恢复异常处理、恢复备用处理
- 异常处理属性 = 异常属性、处理属性
6.1.3 IEEE754有关异常处理的基本规则
1、下文的语言标准或语言定义,是指的类似于C++、java等之类的程序语言的标准,如C++11、C++14等就是C++的语言标准。
2、实现,是指的按语言标准做出来的软件(app),通常是指编译器,如Visual Studio C++等,就是C++语言标准的实现。
3、有关属性与异常处理的规则
- 规则1:属性在逻辑上与块相关联。语言标准应定义并要求实现提供备用异常处理属性与块的关联方法。
- 规则2:属性值的语法和作用域是由语言定义的,通常,属性值的作用域是与它相关联的块
- 规则3:用户启用备用异常处理属性的方法是由语言标准定义的。
- 规则4:用户可以为属性参数指定一个常量值,属性值也可以是动态模式的值。
- 规则5:语言标准应该(注意:这不是必须的)定义IEEE754中规定的所有备用异常属性。
- 规则6:语言标准还应为IEEE754中定义的5种异常各定义至少一个延迟备用异常处理属性。注:IEEE754中定义的5种异常是指:无效操作、不精确的、下溢、溢出、除零。
- 规则7:IEEE754中定义的五种必须发送的异常信号会调用发送异常信号的默认或备用处理。注:IEEE754并未规定怎样调用异常处理。
- 规则8:语言标准还应定义一个异常列表,包含IEEE754中定义的5种异常
- 规则9:备用异常处理程序指定了异常列表,以及如果列出的每个异常被发送时,应采取的动作。
- 规则10:对于每种异常,实现必须提供相应的状态标志。
4、各种异常处理的具体处理方式
- 规则11:默认的不间断异常处理(即,默认异常处理):
将交付一个默认结果、继续执行、并提升相应的状态标志(除精确下溢外)。
- 规则12:即时备用异常处理:
如果指示的异常被发送,那么尽快放弃相关块的执行并执行处理程序(handler)块,然后根据语言的语义在相关块正常终止后继续执行的地方继续执行。
- 规则13:延迟备用异常处理块:
如果指示的异常被发送,则按默认方式处理它,直到相关的块正常终止,然后执行处理程序块(handler block),然后根据语言的语义,在相关块正常终止后继续执行的地方继续执行
- 规则14:即使转移(transfer):
如果指示的异常被发送,则尽快转移(transfer)控制;可能没有返回值。
- 规则15:延迟转移(transfer):
如果指示的异常被发送,则按默认方式处理它,直到相关的块正常终止,然后转移(transfer)控制;可能没有返回值。
- 语言标准可能会提供一些转移习语(idiom),例如:
- 规则16:break:
根据该语言的语义,放弃关联的块并在相关块正常终止后继续执行的地方继续执行。
- 规则17:throw exceptionName:
导致exceptionName不被本地处理,而是向作用域中的下一个处理发送信号(signaled),根据该语言的语义,可能是调用当前子程序的函数。调用者可能在默认情况下处理exceptionName,或者通过备用处理,例如向下一个更高级别的调用子程序发送exceptionName信号。
- 规则18:goto标签:
即,直接跳至标签处,根据语言的语义,标签可以是局部的或全局的。
- 规则19:恢复备用异常处理:
恢复备用异常处理属性将根据指定的恢复属性处理隐含的异常,并恢复相关块的执行。下面列举几个典型的恢复属性作为示例(IEEE754共定义了7个,本文不详细讲解恢复备用异常处理)
- default :在相关的块中提供默认的异常处理,该属性会提升标志,
- raiseNoFlag:提供默认的异常处理,但不提升相应的状态标志。
- recordException:提供默认的异常处理,并在指定提升标志时记录(record)相应的异常。
- substitute(x):可指定任何异常,将这种异常运算的默认结果替换为变量或表达式x。注:substitute:替代,代替
6.2 理清IEEE754的异常处理
6.2.1 基本程序结构约定
本文使用类似编程语言的语法来讲解IEEE754的异常处理,因此,作如下基本规定
- 本文使用大括号对“{}”表示程序块
- 使用类似C或C++的函数的语法。
- 一个完整的程序语句以分号结束。
- 在双斜杠“//”之后的内容为注释
- if ( a = b) {语句1;}
以上语句表示,如果a和b相等,则执行大括号内的语句1,否则不执行(即跳过)大括号对中的内容。
6.2.2 属性命名约定
1、为什么要命名
IEEE754并未对所有的异常类型、异常处理方式、属性指定标准的名称,也就是说,这些名称需要由实现或语言标准来定义,因此,我们也可以自行对其命名。
2、为方便讲解,本文自行命名如下名称:
- 舍入属性 = RoundAttr
其取值为IEEE754中规定的5种舍入属性
- 异常处理属性= allExcep
表示IEEE754规定的所有可能的异常处理方式,可将以下所有属性作为其值
- 默认异常处理属性 = defaultExHAttr
- 备用异常处理属性 = ExHAttr
可将以下3个属性作为其值
- 恢复备用异常处理属性 = ResExHAttr
可取值IEEE754中定义的7个属性值,如default 、raiseNoFlag、recordException等
- 即时备用异常处理属性 = ImmExHAttr,暂无可取值
- 延迟备用异常处理属性 = DelExHAttr,暂无可取值
3、本文不对属性和属性值作一严格区分,一个属性即可作为属性使用,也可作为属性值使用,如,备用异常处理属性ExHAttr可取值之一为ResExHAttr 属性(恢复备用异常处理属性),而ResExHAttr属性又可取值default、raiseNoFlag等。
6.2.3 IEEE754异常处理的基本规则的设计
1、注:以下的“本文规定”是指的需要由语言标准去规定,或需要由实现(如编译器)去完成的任务,本文不需关心“本文规定”的内容是如何做到或完成的,因为那不是本文重点,那是诸如编译器的事,本文只负责设计一个基本的流程,以讲清楚IEEE754的异常处理规则。
2、本文假设程序从f()函数开始执行。
3、本文使用try块以表明该程序块会产生异常。
4、本文使用setExAttr()函数,该函数可完成IEEE754的以下规则
- 根据规则1,本文规定,使用setExAttr()函数来设置异常处理属性,以决定怎样处理发送的异常。
- 根据规则2,本文规定,异常处理属性的作用域从setExAttr()函数开始直到try块结束。
- 根据规则3,本文规定,启用备用异常处理属性的方法是通过setExAttr()函数的参数传递的,如setExAttr (ImmExHAttr),其中参数ImmExHAttr就表示启用即时备用异常处理属性。
- 根据规则4,本文规定,调用setExAttr()函数时的参数就是一个属性值。如setExAttr (ImmExHAttr),其中,ImmExHAttr就是属性值。
- 根据规则5,本文规定,setExAttr()函数的参数可以是IEEE754中定义的所有备用异常属性
- 根据规则6,本文规定,setExAttr()函数设置为延迟备用异常处理属性时,可以发送IEEE754中定义的5种异常。
5、xxx()函数
- 本文使用的函数xxx是类似于IEEE754中规定的会发送异常的运算,如division()、squareRoot()等运算
- 根据规则7,本文规定,当xxx()函数发送异常时,必须按setExAttr()函数设置的方法调用y()或z()函数以处理异常,至于怎么完成这种调用任务的,不是本文重点。
6、本文使用函数y作为备用异常处理函数,z作为默认异常处理函数。
7、根据规则8,在y和z中的一系列if语句就是一个异常列表(示例只包含了部分列表)。该列表其实可以视情况只列出需要处理的异常,而不需要完全列出来
8、根据规则9,if语句后的程序块就是处理异常的动作。
9、setFlag()函数用于设置异常的状态标志。
10、理清哪些程序块的代码是由系统(如编译器)完成的,哪些需要由用户自已编写完成
- 1)、本文的f()函数,需要由用户完成,这是用户自已编写的函数,只是需要用到可能会发送异常的IEEE754规定的运算而已,如division()、squareRoot()等运算
- 2)、setExAttr()函数是由系统完成的,且其内部实现了IEEE754的一系列规则,这些规则都应由系统来完成。因为,用户仅是使用者,不是设计者。
- 3)、try块需要用户完成,这需要用户在调用某个IEEE754的运算时,预先判断该运算是否会发送异常,若会发送异常,则包含在try块内。
- 4)、xxx()函数应由系统完成,因为该函数代表的是IEEE754规定的运算,在IEEE754中,某些运算是与某些运算符等同的,如division类似于除法运算符、squareRoot类似于平方根运算符,这些运算的代码不应由用户来编写(用户只是使用者),这就好像“+”运算符的计算规则应由系统完成一个道理。
- 5)、默认异常处理函数z,应由系统来完成
- 6)、备用异常处理函数y,应由用户来完成,系统的默认处理方式解决不了,就需用户自已解决了(即,需要使用备用产品了)
11、通过以上规定,再根据IEEE754对异常处理的具体方式,我们可以列出一个如下所示的IEEE754处理异常的程序结构,IEEE的异常处理流程是很简单的,如下图XXX所示
12、IEEE754处理异常的程序结构如下所示
13、下图为IEEE754即时、延迟备用异常处理和默认异常处理的程序执行流程
14、转移控制是指的把异常处理转移到本地之外的其他地方处理异常(throw),或者直接跳过(break或goto),其程序执行流程如以下两图所示
6.2.4 异常处理现实示例
1、为便于理解异常处理的具体规则,现在以现实中的示例来帮助理解异常处理的各种情形,以及其作用、逻辑、适用情况。
2、某公司正在进行有多个任务的大型工程研究,员工A突然生病了(产生异常),需要送到医院(异常处理程序),并进行医治(处理异常),于是向上级B汇报(发送异常),以上情况与异常的关系类似于
生病 = 异常
医院 = 异常处理函数或异常处理程序
医治 = 处理异常
向上级汇报 = 发送异常
现有以下几种方式处理员工A的异常情况
-
1)、默认处理:向上级B汇报情况,B告诉A“再重的病情也不能影响当前任务”(相当于产生一个结果),让A继续执行当前任务,然后无视A的病情,让A继续工作(当然,A带病工作产生的效果好不好就不得而知了),并把A的病情记录下来(相当于设置了异常的状态标志)。这相当于是默认异常处理类型和可恢复模式。
-
2)、即时处理:立即停止并终结A当前的任务,将A立即送到公司的医院X医治,等A医治好之后,再继续执行下一个任务。相当于即时备用异常处理类型和不可恢复模式(仅当前任务不可恢复)
-
3)、延迟处理:让A继续执行当前任务,等当前任务完成后,再将A送到公司的医院X医治,等A医治好之后,再继续执行下一个任务。相当于延迟备用异常处理类型和不可恢复模式(仅当前任务不可恢复)
-
4)、上级B发现公司的医院X不能医治员工A的疾病,需要在公司之外的医院Y医治,于是可采取以下方式处理员工A的异常
-
立即停止A当前的任务并终结该任务,把A转到公司之外的医院Y。相当于立即转移控制
-
让A继续执行当前任务,等当前任务完成后,再将A送到公司之外的医院Y。相当于延迟转移控制。
-
A转到医院Y后,是否被医治,怎样被医治的,公司并不清楚。
-
以上的转移控制方式又可以具体按以下方式执行
-
break方式:把A转移到医院Y之后,不管A是否被医治,都把当前任务终结,并继续执行下一个任务(无需等待A医治好),这差不多类似于把A直接解聘了,A是否被医治,以及是否医治好,都不影响之后任务的执行,仅影响当前任务。相当于转移控制方式的break类型和不可恢复模式(仅当前任务不可恢复)。
-
throw (抛出)方式:将A送到医院Y后,医院Y可能不会医治员工A(如,认为不严重),让A返回原岗位继续当前任务(类似于默认处理);也可能会继续使用公司的医院X医治,如,医院Y向员工A的上级B的上级C汇报情况,上级C考查情况后,认为该员工能在公司的医院X医治,员工A仍在医院X医治。这类似于转移控制方式的throw方式,是否是可恢复模式需视情况而定。
-
goto方式:直接跳过由A所影响的某些工作(可能直接跳过A的当前任务,也可能只跳过当前任务的一部分,也可能直接跳过多个任务),然后继续执行剩下的任务,此处完全无视A的病情。相当于控制转移的goto标签和不可恢复模式(当前任务或部分任务不可恢复)。
-
5)、小结:
由以上规则可见,无论哪种处理方式都称不上是完全的不可恢复模式,因为,都只终结了整个工程的当前任务,后续任务并未被终结,仍可继续执行,真正的完全不可恢复模式是,一旦发现异常就直接结束整个工程(或整个程序)
有关IEEE754其他剩余章节的内容
IEEE754还讲解了以下内容,本文不作讲解,这些内容几乎都是对实现的要求或一些补充的要求
1、推荐的运算:主要定义了一些额外的推荐的不是必须要求的运算函数,如指数运算函数、三角函数、∑求和运算函数、∏运算函数等
2、表达式求值规则:主要是表达式的运算规则,如赋值时的舍入问题、求值顺序等,这些规则与程序设计语言中的规则是类似的,且应由实现(如编译器)来完成,所以意义不大,本文不对其讲解。
3、Reproducible (可再生的、可复制的)浮点结果。
作者:黄邦勇帅(原名:黄勇)
2021-11-29