【问题标题】:Is the 16-bit math in this program invoking undefined behavior?这个程序中的 16 位数学是否调用了未定义的行为?
【发布时间】:2018-06-26 00:58:34
【问题描述】:

前几天我将我的 Windows 构建环境从 MSVC2013 升级到 MSVC2017,你瞧,我的程序中的一个函数多年来一直运行良好(在 g++/clang 下仍然运行良好)突然开始给出不正确的结果使用 MSVC2017 编译。

我能够重新编写函数以再次给出正确的结果,但这种经历让我感到好奇——我的函数是否调用了未定义的行为(直到现在才恰好给出正确的结果),或者代码是否定义明确并且MSVC2017 出了问题?

下面是一个简单的程序,展示了我重写之前和之后的玩具版本。特别是,当使用值为 -32762 的参数调用时,如下所示的函数 maybe_invokes_undefined_behavior() 是否会调用未定义的行为?

#include <stdio.h>

enum {ciFirstToken = -32768};

// This function sometimes gives unexpected results under MSVC2017
void maybe_invokes_undefined_behavior(short token)
{
   if (token >= 0) return;

   token -= ciFirstToken;  // does this invoke undefined behavior if (token==-32762) and (ciFirstToken==-32768)?
   if (token == 6)
   {
      printf("Token is 6, as expected (unexpected behavior not reproduced)\n");
   }
   else
   {
      printf("Token should now be 6, but it's actually %i\n", (int) token);  // under MSVC2017 this prints -65530 !?
   }
}

// This function is rewritten to use int-math instead of short-math and always gives the expected result
void allgood(short token16)
{
   if (token16 >= 0) return;

   int token = token16;
   token -= ciFirstToken;
   if (token == 6)
   {
      printf("Token is 6, as expected (odd behavior not reproduced)\n");
   }
   else
   {
      printf("Token should now be 6, but it's actually %i\n", (int) token);  
   }
}

int main(int, char **)
{
   maybe_invokes_undefined_behavior(-32762);
   allgood(-32762);
   return 0;
}

【问题讨论】:

  • 你用的是哪个版本的VS2017?我无法重现 v15.7.3(x86 或 x64,使用 /Od 或 /Ox)的问题。
  • 我正在使用 v15.7.3,IIRC。
  • 看到人们无法重现该问题,因此发布确切的编译器版本和编译开关以进行重现会有所帮助
  • 他们的关键问题当然是 -(-32768) 简而言之,数学溢出了未定义的行为。 MSVC 错误似乎是它正在将上面的有效代码转换为另一种形式,并意外引入了该 UB。
  • 您确实需要告诉我们您平台上SHORT_MINSHORT_MAX 的值,以便确定定义了什么和没有定义什么。没有这些信息,这个问题几乎没有意义。

标签: c++ language-lawyer undefined-behavior msvc14


【解决方案1】:

如果 (token==-32762) 和 (ciFirstToken==-32768)?

token -= ciFirstToken;

否(简短回答)

现在让我们逐条分解。

1) 根据expr.ass 的复合赋值,-=

E1 op= E2 形式的表达式的行为等价于 E1 = E1 op E2 除了 E1 只计算一次。

表达式:

token -= ciFirstToken;

相当于:

token = token - ciFirstToken;
//            ^ binary (not unary)

2) additive operator (-) 对算术类型的操作数执行usual arithmetic conversion

根据expr.arith.conv/1

许多二元运算符需要算术或 枚举类型导致转换并产生类似的结果类型 大大地。目的是产生一个通用类型,这也是 结果。这种模式称为通常的算术转换, 定义如下:

(1.5) 否则,应在两个操作数上执行积分提升

3) 然后将两个操作数提升为 int

根据conv.prom/1

整数类型的 prvalue 不是 boolchar16_­t、char32_­t 或 wchar_t 其整数转换秩小于int 的秩 如果int 可以代表所有 源类型的值;

4) 整数提升后,无需再进行转换。

根据expr.arith.conv/1.5.1

如果两个操作数的类型相同,则不需要进一步转换。

5) 最后,表达式的未定义行为根据expr.pre定义:

如果在计算表达式期间,结果不是 以数学方式定义或不在可表示值的范围内 它的类型,行为是未定义


结论

所以现在替换值:

token = -32762 - (-32768);

在所有整数提升之后,两个操作数都在 INT_MIN[1]INT_MAX[2] 的有效范围内.

并且经过评估,数学结果 (6) 然后隐式转换为short,在short的有效范围内。

因此,表达式是格式良好的

非常感谢 @MSalters、@n.m 和 @Arne Vogel 帮助解答这个问题。


Visual Studio 2015 MSVC14 Integer LimitsMS Integer Limits 定义:

[1] INT_MIN    -2147483648
[2] INT_MAX   +2147483647

SHRT_MIN    –32768
SHRT_MAX   +32767

【讨论】:

  • “Visual Studio 2015 MSVC14 Integer Limits”很好,但它是 2017 年的可以解释这个问题 - 尽管我对此表示怀疑。
  • @chux,这是integer limits 的 MS 参考。很可能没有任何变化,因为标准没有太大变化。
  • @chux Microsoft 是向后兼容的王者——他们将long 保留为 32 位,而不是像其他人一样迁移到 64 位。他们改变这些限制的可能性为零。
  • @MarkRansom:通过将long 解释为“至少 32 位的最小尺寸”,他们有什么优势?实际上想要 64位的代码有什么理由不应该使用int64_tlong long
  • @supercat 我根本没有批评他们的选择,只是用它作为他们优先考虑的证据。
猜你喜欢
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 2018-08-19
相关资源
最近更新 更多