Chinaunix首页 | 论坛 | 博客
  • 博客访问: 49580
  • 博文数量: 19
  • 博客积分: 1400
  • 博客等级: 上尉
  • 技术积分: 215
  • 用 户 组: 普通用户
  • 注册时间: 2008-12-08 11:40
文章分类

全部博文(19)

文章存档

2011年(1)

2009年(18)

我的朋友
最近访客

分类: C/C++

2009-04-22 16:07:43

昨天提到用类似 AOP的方法自动生成logging statement。那么哪些函数的entry/exit要记log,哪些不要呢?这可以引入类似J2SE 5.0和.NET的attribute/metadata的方式,给要记或者不要记log的函数打个标记。然后预处理器会读这些标记来判断。还有就是可以 控制log的层次,比如3层,那么预处理器会进行静态分析,call stack最顶层的3层函数调用会被记log,而3层以下的就不会。事实上,说这么多已经构成一个产品的想法了:) 我知道ParaSoft的C++Test可以自动对源代码进行静态分析,根据状态集和参数类型自动生成unit test程序。不过不知道有没有现成产品可以自动生成logging代码的。

如果找不到现成的产品,也不愿意自己写一个这样的预处理器,那么也有土办法:我知道有些产品的coding standard要求所有函数都有显式的return语句。而且,他们会把return重定义成一个宏,那个宏会先记trace log然后再返回。如果编译器有预定义的__FUNCTION__宏(比如GCC就有,好像还定义了__PRETTY_FUNCTIION__,C99标 准则定义了__function__),或者开发者不辞辛劳地在每个函数前重新定义这个宏,那么这样也起到了半自动生成trace的效果。

那么,有没有哪些log必须手工写入,而不应自动生成呢?有的。我们先对log粗粗分个类:fatal,error,trace。(可能 还可以分得更细一些,比如还有event啦,debug啦等等。)其中,trace指为了方便追踪程序执行到哪里了而留下的痕迹,前面已经提到可以自动或 者半自动地生成;fatal和error则分别指致命错误(程序除了退出别无选择)和非致命错误(程序可以忽略或者回滚一些操作而继续执行下去)。这些信 息应该跟error handling机制结合起来,由error handling机制负责记log。

比如,预定义的assert宏会在屏幕上打一行出错信息然后退出程序。我们可以重新定义它,让它在调试版本中打出错信息然后自动激活调试 器(比如在某些环境中是用嵌入汇编调中断3),在发布版本中的行为是把出错信息写进log,然后根据产品整体的错误处理机制,或者温柔地退出程序/重启程 序,或者抛指定类型的异常,或者在某些特殊情况下也可以只记log其他什么都不做。

再比如,还可以定义enforce宏,其实就是assert的表达式版本(assert语句本身不能作为表达式)。还有,Java里面有 个好用的printStackTrace()函数,在C++中对应fatal log也应该实现类似的机制以便于定义问题。这里不具体展开说了。推荐几篇CUJ上的文章:

Assertions - http://www.cuj.com/documents/cujcexp2104alexandr/
Enhancing Assertions - http://www.cuj.com/documents/cujcexp0308alexandr/
Stack Trace Assertions using COFF - http://www.cuj.com/documents/cuj9706pescio/
Enforcements - http://www.cuj.com/documents/cujcexp2106alexandr/
Rich Error Information - http://www.cuj.com/documents/cuj0503calcote/

下面说说error handling机制。

典型的error handling机制有2种:一种是比较笨的方法,一层一层往外传error code。这样做,那么代码量少则会膨胀2~3倍,多则9倍。(一些电信领域的软件90%的代码都是错误处理。集成电路上也常常有90%的部分是做错误处 理的。)代码的可维护性和可读性也要降低至少2~3倍。原因很简单:在稳健的软件中是不允许忽略某些错误从而让系统陷入某种不一致状态的。于是每一行代 码"f();"都必须扩展成好几行:if (f()==success) { ... } else { ... } 其中else里面做的事基本上很类似,要么是清除在这一层分配的资源然后把错误码继续往上一层传递,要么就是goto到某个标号为clean up的地方,在那个地方清楚资源然后把错误码往上层传。这个if else goto结构会重复出现无数次。极度地丑陋,并且导致代码极度地膨胀。而且一不小心在哪里遗漏了就会静悄悄地出错。因为错误码是可以忽略的,编译器发现不 了。忽略之后,在运行时也不会马上报错,只是程序静悄悄地进入了不一致的状态,然后在之后某个阶段毫无征兆地出错了。

每个函数都可能会造成两条处理路径:正常和异常。但是函数却只有一个返回值。怎么办呢,于是一些不优雅的方案产生了:全局的errno变 量啦,GetLastError啦,或者用输出参数啦。而且,用传递error code的方法来做错误处理其实是试图用一维的执行顺序去涵盖这两个不同的维度,这就好比用一张平整的纸去包一个立体的东西(想象一下,用一张报纸去包一 个篮球,或者包一颗巨大无比的钻石,会包成什么样)——结果一定是包得很不服帖,而且纸有好多面积浪费在重叠之处,甚至可能会包得皱巴巴地极不美观。

所以,在C++世界中,现在人们一般都是在边界处用错误码传递,而在模块内部完全用异常。

边界处(也就是接口处)用返回值来表示错误是为了让接口尽量通用。比如操作系统的API是一个边界,模块向外部提供的接口也是一个边界,在 这个边界一般就不用异常。因为如果Win32 API用了异常,那么这个API就不能被C语言程序或者其他一些不支持异常的语言写的程序调了。

但是用支持异常的语言调用API或者使用模块的人一般都会通过一个类型安全并且把错误码转换成异常的封装层来用。比如JDK就是对很多平 台的底层API的这样的封装,比如.NET Framework就是对Win32 API的类型安全的封装。比如ACE的wrapper facade层是对socket API的类型安全(并且跨平台)的封装。比如STL是对C API的类型安全的封装。Java和.NET的封装层会损失效率,但C++的封装只损失编译期效率,运行期效率则几乎毫无损失。

在模块内部则毫无疑问应该尽可能地用Enforce/Assertion宏封装出错处理,或者直接用异常。这是因为异常是类型安全的,而 且异常路径和正常路径分离,这符合SoC(Separation of Concerns,分离关注焦点)的原则。而且模块内部肯定是用同一种语言写的,同一个编译器编译的,不存在尽量通用的问题。因为都是自己用自己。而且异 常不能被忽略,有问题会很快被发现。

总的原则是,如果用到的API返回了错误码,那么可以检测错误码并处理,但不要继续往上传递错误码。如果错误可以在这一层处理,那是最好,把错误处理掉,不要抛异常。否则就应该抛异常,不管是直接抛还是通过Assert或者Enforce宏抛。

来引经据典一下吧:C++ Coding Standards第72条是这样说的:When harmed, take exception: Prefer using exceptions over error codes to report errors. Use status codes (e.g., return codes, errno) for errors when exceptions cannot be used (see Item 62), and for conditions that are not errors. Use other methods, such as graceful or ungraceful termination, when recovery is impossible or not required.

我再加个注释:其中提到的第62条就是指模块边界(比如,不要把异常从一个DLL抛到另一个DLL)。

【转载自:】!1p0i3yoUKRgnWt0UyAV1FMog!594.entry

阅读(686) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~