微软的 C++ 编译器将自增自减炖成了一锅汤

《由一个自增引发的问题》的后续,用实验和文档证明微软挖大坑。

附赠:瞥一眼,瞧一瞧给微软报告bug特别是技术bug会有多麻烦。


今天凌晨我写了《由一个自增引发的问题》,然后将链接发给了杨彦君。此时已快两点半,想不到他居然还没睡,说是战M(战女神 Memoria)出了汉化要使劲推……

上午我有课,中午回到宿舍的时候看到了反馈。

clang 的结果没错

微软家的 CL 又一次出丑了

跟你说的一样,++慎重

我甚至有点理解为什么Python和Ruby要删去++了

然后我提出了一个假设:其实如果不计编译效率的话,全部用栈来实现一个虚拟机语言,其 INC 指令就不会出错。他回复:“Java 吗?”我构造了一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUSH 3 // stack[0]
PUSH 0 // stack[1]
INC stack[0] // stack[0] += 1
ST stack[2]
INC stack[0]
ST stack[3]
INC stack[0]
ST stack[4]
ADD stack[1], stack[2] // 第一次 pre-increment 的结果
ADD stack[2], stack[3] // 第二次 pre-increment 的结果
ADD stack[3], stack[4] // 第三次 pre-increment 的结果
POP
POP
POP
POP result

对于此例子,他的评论:

clang和llvm组合应该是这种结果

然后我用 Java 做了个实验,证明了 Java 的执行结果是符合预期的(在自增自减上二者语义是相同的):

Java 的结果是正确的

都做到这份上了,不妨将前置后置、自增自减都来试一遍。有兴趣的同学可以编译一下。最终确认了,无论是前置还是后置,无论是自增还是自减,微软出品的 C++ 编译器都能做出一锅大杂烩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 环境:MSC (Microsoft C++ Compiler) 19.0, target 10.0.10240, x64 / Win32
int main()
{
int i;
int j;

// 预期:12 (=3+4+5)
// 实际:9
i = 3;
j = (i++) + (i++) + (i++);

// 预期:15 (=4+5+6)
// 实际:18
i = 3;
j = (++i) + (++i) + (++i);

// 预期:3 (=2+1+0)
// 实际:0
i = 3;
j = (--i) + (--i) + (--i);

// 预期:6 (=3+2+1)
// 实际:9
i = 3;
j = (i--) + (i--) + (i--);

return 0;
}

所以微软的 C++ 编译器在自增自减上病的不轻啊。

从中抽个例子,和昨天的不同的例子,用后置自增吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mov dword ptr [ebp-8],3
mov eax,dword ptr [ebp-8]
add eax,1
mov dword ptr [ebp-8],eax
mov ecx,dword ptr [ebp-8]
add ecx,1
mov dword ptr [ebp-8],ecx
mov edx,dword ptr [ebp-8]
add edx,1
mov dword ptr [ebp-8],edx
mov eax,dword ptr [ebp-8]
add eax,dword ptr [ebp-8]
add eax,dword ptr [ebp-8]
mov dword ptr [ebp-14h],eax

该怎么吐槽呢,和表兄如出一辙?

让我们看看 C++ 规范里是怎么写的吧。由于为了此事专门去花212美刀买一份生效了的标准(“US$212, working on getting this down to $60”)而非草案实在是不划算,我只好下载了2014年11月的工作草案

其中,第106页,5.2.6节规范了后置自增自减(我进行了必要的加粗):

5.2.6 Increment and decrement [expr.post.incr]

The value of a postfix ++ expression is the value of its operand. [ Note: the value obtained is a copy of the original value — end note ] The operand shall be a modifiable lvalue. The type of the operand shall be an arithmetic type or a pointer to a complete object type. The value of the operand object is modified by adding 1 to it, unless the object is of type bool, in which case it is set to true. [ Note: this use is deprecated, see Annex D. — end note ] The value computation of the ++ expression is sequenced before the modification of the operand object. With respect to an indeterminately-sequenced function call, the operation of postfix ++ is a single evaluation. [ Note: Therefore, a function call shall not intervene between the lvalue-to-rvalue conversion and the side effect associated with any single postfix ++ operator. — end note ] The result is a prvalue. The type of the result is the cv-unqualified version of the type of the operand. See also 5.7 and 5.17.

The operand of postfix — is decremented analogously to the postfix ++ operator, except that the operand shall not be of type bool. [ Note: For prefix increment and decrement, see 5.3.2. — end note ]

第115页,5.3.2节规范了前置自增自减(我进行了必要的加粗):

5.3.2 Increment and decrement [expr.pre.incr]

The operand of prefix ++ is modified by adding 1, or set to true if it is bool (this use is deprecated). The operand shall be a modifiable lvalue. The type of the operand shall be an arithmetic type or a pointer to a completely-defined object type. The result is the updated operand; it is an lvalue, and it is a bit-field if the operand is a bit-field. If x is not of type bool, the expression ++x is equivalent to x+=1 [ Note: See the discussions of addition (5.7) and assignment operators (5.17) for information on conversions. — end note ]

The operand of prefix — is modified by subtracting 1. The operand shall not be of type bool. The requirements on the operand of prefix — and the properties of its result are otherwise the same as those of prefix ++. [ Note: For postfix increment and decrement, see 5.2.6. — end note ]

最后的加粗意思就是说,y = ++xy = x += 1(由于运算优先级有保证,括号省略了)是等价的。(再展开,就是和 y = x = x + 1 等价了,再结合赋值表达式的值规则可以得到结果。)

结论就是:微软你挖了一个至少17年的大坑(时间肯定更长),编译器版本从 12.0(对应 Visual C++ 6.0)到 19.0(对应 Visual Studio 2015),现在还没填上。再往前的编译器我没试过,就当“可能有”吧(这种bug一般就是之前没解决的遗留,较少是新繁殖的虫子)。这个坑一些“思想奇异”的开发者说不定什么时候就往里面跳了,而且如果没有反汇编还爬不出来;复杂的程序你还想慢慢反汇编找?


我认定这是一个bug,想报告给微软,不过想不起来怎么报告。于是我 Google 了一下“microsoft bug report”。

排名在第一的,微软的帮助论坛上的帖子和吐槽大家就自己看吧。

第二项是 Microsoft Connect。可是点击进去后,页面非常吓人:

Microsoft Connect 分类主页

我就是想报告一个编译器bug而已啊!您老即使产品线那么大,一眼看上去也没有一个专门给技术人员报bug的地方啊!(这一点从 Visual Studio Code 终于用了自己的公开bug反馈系统开始得到了改善,虽然在 0.9.1 之前隐藏很深……)

在这方面,你看看隔壁 Apache,看看 Mozilla……

我认了。就选 Visual Studio and .NET Framework 吧。

分类下的子类别

你要我选哪个?我期望的是一个“CL”或者是一个“Visual Studio Internal Fault”这样的分类啊!

好吧,我选择 Visual Studio 2015。那么你告诉我,我希望上传示例和截图来帮助提高反馈质量,你们也说了可以上传附件,但是这些是怎么回事:

  • 一堆的脚本加载失败
  • 由此带来的不可上传附件和区域空缺
  • “©”这样的 HTML 5 常用符号(©)怎么就没有解析了?

Chrome 页面加载报错

我认为 Chrome 可能不适应微软自家的菜肴,于是我换上了微软的亲女儿 Edge:

Edge 页面加载失败

你是在逗我吗?

  • MSDN 上的 C++ 版块充斥着语言应用的帖子,不是提交错误的地方。
  • 这位老兄(年份很早了)想给微软提交一个崩溃报告,但是石沉大海。

以下两位是我搜索“report a compiler bug to visual studio”看到的。

  • 这位老兄实在是幸运,在 Microsoft Connect 提交的编译器错误最终被修复了。
  • 这位老兄的运气就很差,提交了一串bug都没有得到好的反馈,一位善良的客服陪着磨嘴皮子。

看到这里我心都凉了。

分享到 评论