一个天真的bug

2013年06月27日

本人写C/C++的,虽非科班出生,但是总是特别有兴趣,于是时常捣鼓。

新写的代码出了个很是奇怪的bug,基础不牢,犯了一个想当然的错误,于是乎查了好几天才找到原因。因为想要把自己写过的一些代码凑起来,弄一个自己的代码库。

因为想要使代码更具有可移植性,所以当然对一些基本的变量类型要做typedef,而typedef的类型来自于Makefile中给编译器的-D选项。基本代码如下:

#if ElemType == float
typedef float Doub;
#elif ElemType == double
typedef double Doub;
#endif

于是乎,无论我把ElemType宏定义为float还是double,Doub的类型总为float。也就是说条件ElemType == float总成立。

这就奇怪了,我一直以为是我在这个文件中引用了其他文件,而那个文件里面宏定义了ElemType为float从而覆盖了我的宏定义。于是乎我把所有的源代码翻出来找还有哪些地方做了ElemType的宏定义,肯定是找不到的撒。然后我想,是不是ElemType没有定义过,编译器会认为此条件永远成立,于是用#ifdef检查,它已经被定义了啊!我开始有点抓狂了!

偶然间,我发现把它改成Elemtype还是有Doub总为float型,然后顺藤摸瓜发现随便改成啥Doub都为float型,接着把两条typedef换一下,Doub就永远变成double型了。原来,问题的原因在于#if ElemType == float总是满足条件!为什么会这么奇葩,赶紧翻翻条件编译,原来#if后面只能跟常量,编译器根本不会判断条件表达式!只要不为0就永远为真!

我真是太天真了,想当然地就写出了这样的bug。我想,要是有个稍微懂行点的人一提点马上就明白了。唉,旁边师兄弟都不知道条件编译是啥玩意儿,写的都是大一的C水平的代码。做科研的有一颗码农蠢蠢欲动的心,真心伤不起啊!

其实把代码改成这样就成了

#ifndef ElemType
typedef double Doub; // default floating type
#else
typedef ElemType Doub;
#endif

=============================================================

好吧,谈到宏定义的问题,在这里多说一句。虽然宏定义是一个很好的简化代码的方式,但是绝对是杀敌一千,自损八百,因为它把编译器拒之门外,所有的工作都是在编译器进入之前就完成了。于是,一旦出现问题就会有一些非常奇怪的现象或错误。

“Numerical Recipes” 是一本数值计算的经典教材,里面提供了大量实用的算法。它提供一个C++的版本,但是,代码质量确实不敢恭维。它并不适合当做一个库来实用,仅适合拷贝一些片段到自己的代码中去。

于是,我希望把它改造为一个库,可以让我随意的实用。我给它添加了防止重复包含的宏,添加了无名名字空间来防止出现函数重定义,有空了再把文件包含关系梳理一下。这当中我遇到一个很奇怪的问题,我发现当先包含头文件nr3.h(包含NR代码的类型定义)再包含omp.h(openmp要用的)或cassert(断言)的时候会出现问题,说是表达式后差一个分号或什么的。后面两个是标准库包含的,我不敢怀疑它有问题。问题只能是出在nr3.h上,于是我分段注释掉它的代码来观察到底是哪儿出了问题。原来问题出在一个宏定义上,它定义了一个宏叫throw(message)用来实现错误输出,然而并没有在文件结束后去掉这个宏。于是,当omp.h中出现需要标准的throw时就出现问题了。解决办法可以是在文件尾把它undef掉,或者换一个不常用的宏名。

此外,介绍一个小trick,用我改造的宏定义做例子,用来将一个含有多个语句的宏包装为一个函数

#define throw(message) \
            do{\
                printf("ERROR: %s\n     in file %s at line %d\n", message,__FILE__,__LINE__);\
                throw(1);\
            }while(0)

宏是不能递归展开的,这里用while(0)实在太巧妙了。