该程序中的 16 位数学是否调用了未定义的行为?

Is the 16-bit math in this program invoking undefined behavior?

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

我能够重写函数以再次给出正确的结果,但这段经历让我感到好奇——是我的函数调用了未定义的行为(直到现在恰好给出了正确的结果),还是代码很好——已定义且 MSVC2017 有问题?

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

#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;
}

does this invoke undefined behavior if (token==-32762) and (ciFirstToken==-32768)?

token -= ciFirstToken;

否(简短回答)

现在让我们逐条分解。

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

The behavior of an expression of the form E1 op= E2 is equivalent to E1 = E1 op E2 except that E1 is evaluated only once.

表达式:

token -= ciFirstToken;

相当于:

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

2) additive operator (-) performs usual arithmetic conversion 用于算术类型的操作数。

根据expr.arith.conv/1

Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result. This pattern is called the usual arithmetic conversions, which are defined as follows:

(1.5) Otherwise, the integral promotions shall be performed on both operands.

3) 然后两个操作数都被提升为 int.

根据conv.prom/1

A prvalue of an integer type other than bool, char16_­t, char32_­t, or wchar_­t whose integer conversion rank is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type;

4) 整数提升后,不需要进一步转换

根据expr.arith.conv/1.5.1

If both operands have the same type, no further conversion is needed.

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

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined


结论

所以现在替换值:

token = -32762 - (-32768);

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

然后求值后,数学结果(6)被隐式转换为short,在short的有效范围内。

因此,表达式格式正确

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


Visual Studio 2015 MSVC14 Integer Limits and MS Integer Limits 定义:

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

SHRT_MIN –32768
SHRT_MAX +32767