【问题标题】:Getting consistent output from printf() between compilers从编译器之间的 printf() 获得一致的输出
【发布时间】:2021-01-26 11:37:02
【问题描述】:

我注意到 printf("%#g\n", 0.0) 在任何 gcc/clang 版本与 Windows 7 上的 Visual Studio 2019 (截至今天的最新版本)提供不同的输出。

gcc/clang 给出 0.00000(总共 6 个数字,. 之后的 5 个)而 VS 给出 0.000000(总共 7 个,. 之后的 6 个)。类似地,"%#.8g" 在 gcc/clang 中总共给出 8 位数字,而在 VS 中总共给出 9 位数字。

问题:

  • 标准对此有何规定?编译器/标准库之一有问题吗?
  • 我只在本地(Windows 7)的 VS 中看到这种行为,但在 Azure Pipelines(最近的 Windows Server)上看不到。哪些特定的编译器版本/标准库版本/操作系统会受到影响?
  • 有没有办法在编译器之间获得一致的输出?

【问题讨论】:

  • @MarkBenningfield:不,这不是重复的;那个地址是打印的值,而不是打印的位数。
  • @EricPostpischil:同意,但它确实解决了实现不同的问题,其中一个答案提供了几种解决方法以在两个平台上保持一致性。
  • 也许可以像if(x) printf("%#g\n", x); else printf("%#.6g\n", x); 等一样简单。

标签: c visual-c++ printf


【解决方案1】:

这是一个错误

您在 Visual Studio 中使用的 C 实现存在缺陷。以下引用来自 C 2018。相关文本在 2011 年和 1999 年标准中实际上是相同的(在 1999 年,不等式使用文本而不是使用“>”和“≥”的数学符号来描述)。

首先,在这种情况下,# 表示将生成一个小数点字符,并且不会删除尾随零。它对删除尾随零之前应该产生的位数没有影响。 7.21.6.1 6 表示“……对于aAeEfFgG 转换,浮点转换的结果number 总是包含一个小数点字符,即使它后面没有数字……对于gG 转换,不会从结果中删除尾随零……”这会使g 规范中的“……除非使用 # 标志,否则将从结果的小数部分中删除任何尾随零,如果没有剩余小数部分,则删除小数点字符。”

其次,对于零值,g 格式的规则说使用f 格式。这是因为g(或G)的规则取决于e 格式使用的指数和要求的精度:

  • 对于e,7.21.6.1 8 表示“……如果值为零,则指数为零。”
  • 对于g,它说“……如果非零,让 P 等于精度,如果省略精度,则等于 6,或者如果精度为零,则等于 1……”所以 P在问题中给出的%#g%#.8g 中,em> 是 6 或 8。
  • 文本继续“...如果 P > X ≥ -4,则转换为样式f(或F)和精度P - (X + 1)。”

因此,%#g%#.8g 的转换是使用样式 f 完成的,精度分别为 6−(0+1) = 5 或 8−(0+1) = 7。

第三,对于f,7.21.6.1 8 表示“代表浮点数的double 参数被转换为样式[-]ddd.ddd的十进制符号,其中小数点字符后的位数等于精度规范……”因此,小数点后应分别打印 5 位或 7 位。

因此,对于%#g,“0.00000”符合 C 标准,“0.000000”不符合。对于%#.8g,总共八位数字(小数点后七位)符合,九位数字(八位之后)不符合。

既然你用visual-c++标记了这个,我会注意到C++标准采用了printf的C规范。 C 2017 草案 N4659 20.2 说“C++ 标准库还提供了 C 标准库的功能,经过适当调整以确保静态类型安全。”

补偿错误

错误可能出在 C/C++ 库中,而不是编译器,因此调整源代码通过使用 #if 测试微软宏 _MSC_VER 的值,例如,可能不是一个好的解决方案。 (特别是在编译之后,编译器可能会与以后的库一起运行。)

可以在程序启动期间测试库。在用外部作用域定义int PrecisionAdjustment; 之后,这段代码可以用来初始化它:

{
    /*  The following tests for a Microsoft bug in which a "%#g" conversion
        produces one more digit than the C standard specifies.  According to
        the standard, formatting zero with "%#.1g" should produce "0.", but
        Microsoft software has been observed to produce "0.0".  If the bug
        appears to be present, PrecisionAdjustment is set -1.  Otherwise,
        it is 0.  This can then be used to select which format string to
        use or to adjust a dynamic precision given with "*" such as:

            printf("%#.*g", 6+PrecisionAdjustment, value);
    */
    char buffer[4];
    snprintf(buffer, sizeof buffer, "%#.1g", 0.);
    PrecisionAdjustment = buffer[2] == '0' ? -1 : 0;
}

这假设我们在精度 6 和 8 中看到的相同错误存在于精度 1。如果不存在,则可以轻松进行适当的调整。

【讨论】:

  • 感谢您的回答!我想知道您是否可以重现该问题(如果您可以访问 MSVC)。我在本地只有一台 Windows 7 机器,我很困惑为什么问题没有出现在 Azure CI 上。
【解决方案2】:

您问是否有办法在编译器之间获得一致的输出。我怀疑是否有直接的方法,我知道这有多令人沮丧。如果额外的 0 位给您带来问题并且必须修复,您将不得不采用某种解决方法。在这种情况下,我可以想象三种截然不同的方法。

  1. 如果 Visual Studio 仅在尝试打印默认位数时出现错误行为,请尝试明确显示位数。也就是说,您可以尝试使用"%#.6g",而不是"%#g"。如果这在两个平台上都为您提供了相同的结果,那么您就完成了。

  2. 如果 Visual Studio 总是打印多出一个数字,您可以尝试在 Visual Studio 下使用与其他平台不同的格式,使用条件编译 (#ifdef) 来选择要使用的格式。如果 Visual Studio 仅在打印的值为 0.0 时出现问题,则您可能还需要对此进行运行时测试。

  3. 您可以使用snprintf 代替printf 来临时缓冲区,然后使用字符串函数(可能是strstr)查看缓冲区是否错误地以".000000" 结尾,如果是,则将其截断一字符。

如果方法 #1 有效,那就太好了。否则,我会强烈鼓励您使用方法#3,即使它(我承认)是一个大而丑陋的麻烦。但如果可能的话,请远离方法#2。它有两个巨大的问题:(1)条件编译最终成为一个更大更丑陋的麻烦。许多风格指南建议——非常正确地——避免它。 (2) 方法 #2 的更大问题是,如果 Microsoft 修复了他们的 printf 版本以使其正常运行,那么您的代码可能会在此时中断,也许您没有注意到它!方法 #2 与“未来证明”相反。

或者,换句话说,“不要把你必须解决的错误供奉”。

【讨论】:

  • ""%#g", try using "%#.6g" 当x == 0(问题值)时有意义,但通常不是。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-11
  • 2016-08-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-03
相关资源
最近更新 更多