跨编译器的 C 语言 NaN 支持 (纠正)

2

Comments

之前的版本犯了一个重大的错误!我错了……没有经过认真的实验就贴出来……

正文开始前,先看一个式子:

1
x != x

大家觉得,这个东西的返回可能为 true 么?事实上是可能的,只要这个 x 是 NaN,并且这个 C 编译器符合 IEEE 754 的标准。

所谓 NaN,即 Not A Number,不是一个数。这是 IEEE 754 国际浮点数运算标准当中规定的一个特殊值,这个值由于不是一个数,所以有很多奇怪的特性,比如上面这个不等于其本身。

虽然有这么个东西,而且是国际标准,不过我们也知道,这世界上总有那么些公司是不喜欢理会国际标准的,邪恶的 M$ 就是其中之一。而 Visual C++ 6.0 的 C 编译器也就“有幸”成为了少有的不能完全兼容 IEEE 754 的编译器之一。

至此,我们发现一个问题:邪恶的 M$ 的东西中,总是最垃圾的流传的最广,IE6 如此,VC6 也是如此。另外,在此声明一下,这里讲的全部是 C,不是 C++,在 C++ 中另外有一些比较符合标准的方式同时被各个编译器兼容。

OK,回归正题,既然 VC6 的编译器是个渣,可我们有的时候还是不得不让自己的代码与之兼容,于是就有了各种解决办法。

首先明确一下,我们现在需要两样东西,一是一个可以用于赋值的 NaN (在我的程序中作为一个标记值使用),另一个是一个用于判断一个数是否为 NaN 的函数或宏。基于上面对 NaN 的介绍,在一个符合标准的编译器上,我们可以很容易地给出如下宏:

1
2
#define NaN (0.0 / 0.0)
#define IsNaN(x) ((x) != (x))

对于上面这样的 NaN 定义,VC6 不同寻常的会发生编译错误:

1
error C2124: divide or mod by zero

可能也有人会问,难道除 0 不应该是错误么?事实上 IEEE 754 里面就是规定 0.0 / 0.0 = NaN。这里我给出一个我个人的理解:学过高等数学的人大约都会知道,一个无穷小除以一个无穷小,他们的极限可能是无穷大或任何实数,而由于浮点数的精度限制,这里的 0 可能不是真的 0,而是一个很小很小极其趋近于 0 的数,类似无穷小,于是有这样的规定吧。再来看看 IsNaN,这个宏就更无敌了,VC6 的编译器会自作聪明的直接把它优化为 false……

那么对于 VC6 我们该怎么办呢?

查阅了许多资料,最后我们在 MSDN 中翻出了一份年代久远的文档:_isnan。这里的 _isnan 是 VC6 在 float.h 中定义的一个函数,用于校验一个数是否为 NaN。现在的问题就剩下,我们如何生成 NaN,以及如何判断编译器呢?

那么我们来思考一下,除了用零除零,还有什么方法可以生成 NaN 呢?翻看了 IEEE754 标准文档,看到了开平方一个负数也应该是 NaN。这一点应该很好理解,开平方一个负数应该得到一个虚数,而虚数不是一个实数,所以也就 Not a Number 了~于是最后形成了下面一段预处理指令:

1
2
3
4
5
6
7
8
#ifdef _FPCLASS_SNAN
#include <math.h>
#define NaN sqrt(-1)
#define IsNaN(x) _isnan(x)
#else
#define NaN (0.0 / 0.0)
#define IsNaN(x) ((x) != (x))
#endif

虽然调用 sqrt 可能有效率问题,而且额外的需要引用 math 头文件,不过还算几乎完美地解决了 NaN 跨编译器的兼容性问题~事实证明,这段代码可以通过 VC6 的编译器正确地编译并执行。

另外说一点东西,就是如何在 Linux 下用 VC6 的编译器呢?这个问题我想我解决的其实是不完美的,不过也留在这里吧。

首先我下载了一个免安装版的 VC6,然后解压。接着找到了 VC6/VC98/Bin 目录,里面有非常著名的 VC6 编译器的主程序 CL.EXE 以及连接器 LINK.EXE。对于编译器这种纯运算的程序,wine 的兼容性还是比较优美的。不过需要一个额外的 dll 文件支持:mspdb60.dll,这个文件很容易载到,解压到 wine 的系统目录 (~/.wine/drive_c/windows/system32) 就可以了。

接下去就是如何编译了……我的方法比较老土,是将待编译的文件复制到编译器目录,然后执行类似下面命令:

1
wine CL.EXE 源文件.c /I../Include /o可执行文件.exe /link /LIBPATH:../Lib

其中“源文件.c”和“可执行文件.exe”是很容易理解的,“/I../Include”是使编译器能知道 include 的文件应该去哪里找,而“/link”表示后面的部分是连接器参数,“/LIBPATH:../Lib”就是表示静态链接库的地址了~

最后再说说如何将 GCC 可以编译的程序移植到 VC6 中。GCC 实现了 C99 标准,但 VC6 因为出现在 1998 年 (怎么正好早一年……不过就算是 99 年出也未必会支持 C99 就是了……),所以不支持 C99,于是所有的变量必须在函数最前面声明,不能混入代码内部。此外,在 VC6 当中,void 指针是不能进行运算的。还有就是不能用“//”开头的行注释,必须使用块注释。这是我移植过程中遇到的主要麻烦。

事实上,在 GCC 当中,可以验证大多数 C89 的限制,只要在编译的时候用如下语句:

1
gcc -ansi -pedantic -o 可执行文件 源文件.c

只要把其中所有的 warning 全部消灭掉就可以啦~

当然,最好的方法莫过于在 VC6 的编译器中直接测试了……

最后最后,M$ 实在是……唉……这个世界上不明真相的孩子果然是占大多数的……


关于修改的部分:之前的版本错误的使用 _FPCLASS_SNAN 这一校验函数 _fpclass 返回值当作 NaN 来使用。既然查到了这个,这里也顺便说一下这个东西吧。在 VC 的 float.h 库里面有 _fpclass 这个函数,用于检验一个浮点数的类型,可能的返回值都在 float.h 里面,如下:

1
2
3
4
5
6
7
8
9
10
#define _FPCLASS_SNAN   0x0001  /* signaling NaN */
#define _FPCLASS_QNAN   0x0002  /* quiet NaN */
#define _FPCLASS_NINF   0x0004  /* negative infinity */
#define _FPCLASS_NN     0x0008  /* negative normal */
#define _FPCLASS_ND     0x0010  /* negative denormal */
#define _FPCLASS_NZ     0x0020  /* -0 */
#define _FPCLASS_PZ     0x0040  /* +0 */
#define _FPCLASS_PD     0x0080  /* positive denormal */
#define _FPCLASS_PN     0x0100  /* positive normal */
#define _FPCLASS_PINF   0x0200  /* positive infinity */

2 Responses to “跨编译器的 C 语言 NaN 支持 (纠正)”

  1. KUN LIN Says:
    2010年2月27日 02:43 回复

    可是就连Google的网站也未必都能通过W3C的严格HTML标准检测

Leave a Reply