将从字符串转换的浮点值与字面量进行比较

24

这不是著名的浮点数计算是否存在问题的重复,即使乍一看起来像是。

我正在使用fscanf(file, "%lf", &value);从文本文件中读取double,并将其与一个双精度字面量使用==运算符进行比较。如果字符串与字面量相同,那么使用==进行比较在所有情况下都会返回true吗?

示例

文本文件内容:

7.7

代码片段:
double value;
fscanf(file, "%lf", &value);     // reading "7.7" from file into value

if (value == 7.7)
   printf("strictly equal\n");

预期输出和实际输出为:
strictly equal

但这假设编译器将双精度字面量7.7转换为双精度,与fscanf函数完全相同,但编译器可能使用不同的库来将字符串转换为双精度。或者换句话说:从字符串到双精度的转换是否会产生唯一的二进制表示,还是可能存在轻微的实现差异?在线演示

@ron 是的,可以使用std::strtod,但问题仍然存在。并且它适用于C和C ++。 - Jabberwocky
对于inf和NaN,没有字面量,我想你想要排除它们? - Baum mit Augen
9
在任何有关浮点数学的问题上抛出《浮点数算术应该知道的每个计算机科学家》或《浮点数算术是否已经失效?》就像在任何C语言问题上抛出C标准一样。 - P.P
@BaummitAugen 是的,infNaN被排除在外。 - Jabberwocky
2
我认为这没有一个明确的答案。在我看来,这是一个质量问题。我认为通常情况下不会得到相同的二进制表示形式。 - StoryTeller - Unslander Monica
显示剩余6条评论
4个回答

18

从C++标准:

[lex.fcon]

...如果缩放后的值在其类型的可表示值范围内,则结果是缩放后的值,如果可表示,则结果为缩放后的值,否则选择最接近缩放后的值的较大或较小的可表示值,以实现定义的方式...

强调是我的。

因此,只有当值严格可由double表示时,您才可以依赖于相等性。


1
@YSC 我很惊讶cppreference提到了它。那个网站每天都在进步。 - Richard Hodges
1
你实际上可以加入这个努力:cppreference是一个维基! - YSC
2
@RichardHodges [lex.fcon] 是什么? - Jabberwocky
2
这肯定是最安全的假设。我的经验是,像这样的事情很容易让人受到伤害。首先,我不确定fscanf所做的转换是否与编译器在编译时进行的转换相匹配。另一个问题是,有时值会以高于预期精度的方式存储在寄存器中。但如果表示在双精度浮点数中是准确的,那么看起来应该是安全的。 - Tom Karzes
1
@MichaelWalz 这是 C++ 标准中的一个章节名称。 - Richard Hodges
@Tom 是的,依赖于这一点似乎非常、非常脆弱。“这段代码之所以能够工作,是因为 X 可以被 IEEE-754 双精度浮点数完美表示,而我们的编译器恰好保证了双精度浮点数使用的就是它。” - Voo

17

关于C++,从cppreference上可以了解到

[lex.fcon] (§6.4.4.2)

计算浮点常量的结果是最接近可表示值的值,或者是与最接近可表示值相邻的较大或较小的可表示值,以实现定义的方式选择(换句话说,在翻译期间的默认舍入方向是实现定义的)。

由于浮点文字的表示未指定,因此我认为您无法得出与 scanf 结果的比较结论。


关于C11(标准ISO / IEC 9899:2011):
推荐做法
7.浮点常量的翻译时间转换应该与库函数(例如strtod)执行字符字符串的执行时间转换相匹配,假设两种转换都有匹配的输入,相同的结果格式和默认的执行时间舍入。因此,很明显对于C11,这不能保证匹配。

这听起来很有说服力。所以我将用一些AmostEqual函数的调用替换== - Jabberwocky
2
@MichaelWalz> 你同时标记了C和C++。我想知道它们是否在这方面达成了一致。在C99中,规范附录F,第7.2小节写道:“在翻译期间,IEC 60559默认模式生效:—舍入方向模式是四舍五入[...]”。因此,只要您不更改代码的浮点环境,您的示例代码似乎保证可以在C99中工作。 - spectras
@YSC,您关于 C11 的观点是,翻译时和运行时的转换一致性只是一种建议,而不是要求,对吗? - John Bollinger
@YSC> 这个建议是关于它们应该匹配的事实。然而,附录 F 是规范性文件,提供了翻译时的精确规则,因此通过明确配置浮点环境来保证匹配。 - spectras
1
@Michael 这段代码在x86上失败的一个简单原因是,从内存中读取值只能获得64位精度,而当该值在寄存器中时可能有80位精度。加载常量很可能直接通过寄存器完成,而fscanf加载的值可能已经存储在内存中某个位置。仅出于这个原因,几乎永远不安全假设浮点数之间相等。 - Voo
@spectras,关于翻译时间规则的标准并不代表执行时库字符串函数的匹配;) - YSC

2

没有保障。

你可以希望编译器使用高质量的算法来转换文字,同时标准库实现也要使用高质量的转换,两个高质量算法应该会经常达成一致。

另外,也有可能两者使用完全相同的算法(例如,编译器通过将字符放入char数组并调用sscanf来转换文字)。

顺便说一句,我曾经遇到一个bug,因为编译器没有精确地转换文字999999999.5。将其替换为9999999995 / 10.0后,一切正常了。


2
如果字符串与字面值相同,使用==进行比较在所有情况下都是真的吗?
一个常见但尚未探讨的考虑因素:FLT_EVAL_METHOD
#include <float.h>
...
printf("%d\n", FLT_EVAL_METHOD);

评估所有操作和常量的范围和精度,以 long double 类型为准。

如果返回值为 2,则在 value == 7.7 中使用的数学运算是基于 long double,而 7.7 被视为 7.7L。在 OP 的情况下,这可能会得出错误的结果。

为了考虑到更宽广的精度,分配值时需要移除所有额外的范围和精度。

scanf(file, "%lf", &value);
double seven_seven = 7.7;
if (value == seven_seven)
  printf("strictly equal\n");

在我看来,这个问题比变量舍入模式或库/编译器转换的差异更有可能发生。


请注意,这种情况类似于下面一个众所周知的问题。

float value;
fscanf(file, "%f", &value);
if (value == 7.7)
   printf("strictly equal\n");

Demonstration

#include <stdio.h>
#include <float.h>
int main() {
  printf("%d\n", FLT_EVAL_METHOD);
  double value;
  sscanf("7.7", "%lf", &value);
  double seven_seven = 7.7;
  if (value == seven_seven) {
    printf("value == seven_seven\n");
  } else {
    printf("value != seven_seven\n");
  }
  if (value == 7.7) {
    printf("value == 7.7\n");
  } else {
    printf("value != 7.7\n");
  }
  return 0;
}

输出

2
value == seven_seven
value != 7.7

替代比较

为了比较两个“相近”的double,我们需要定义“相近”的概念。一个有用的方法是将所有有限的double值按升序排序,然后比较它们之间的序列号。double_distance(x, nextafter(x, 2*x) --> 1

以下代码对double的布局和大小做出了各种假设。

#include <assert.h>

unsigned long long double_order(double x) {
  union {
    double d;
    unsigned long long ull;
  } u;
  assert(sizeof(double) == sizeof(unsigned long long));
  u.d = x;
  if (u.ull & 0x8000000000000000) {
    u.ull ^= 0x8000000000000000;
    return 0x8000000000000000 - u.ull;
  }
  return u.ull + 0x8000000000000000;
}

unsigned long long double_distance(double x, double y) {
  unsigned long long ullx = double_order(x);
  unsigned long long ully = double_order(y);
  if (x > y) return ullx - ully;
  return ully - ullx;
}

....
printf("%llu\n", double_distance(value, 7.7));                       // 0
printf("%llu\n", double_distance(value, nextafter(value,value*2)));  // 1
printf("%llu\n", double_distance(value, nextafter(value,value/2)));  // 1

或者只需使用。
if (nextafter(7.7, -INF) <= value && value <= nextafter(7.7, +INF)) {
  puts("Close enough");
}

我差点就点赞了,但是“union”类型游戏破坏了C++别名限制(这也是一个经常出现的话题)。一个良好的解决方案应该使用“memcpy”。 - Arne Vogel
@ArneVogel 对于C++的观点很好。但是这篇文章也标记了C,而这个答案并没有打破别名限制。鉴于fscanf(),OP代码更像是C而不是C++。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接