《由一个自增引发的问题》的后续,用实验和文档证明微软挖大坑。
附赠:瞥一眼,瞧一瞧给微软报告bug特别是技术bug会有多麻烦。
今天凌晨我写了《由一个自增引发的问题》,然后将链接发给了杨彦君。此时已快两点半,想不到他居然还没睡,说是战M(战女神 Memoria)出了汉化要使劲推……
上午我有课,中午回到宿舍的时候看到了反馈。
跟你说的一样,++慎重
我甚至有点理解为什么Python和Ruby要删去++了
然后我提出了一个假设:其实如果不计编译效率的话,全部用栈来实现一个虚拟机语言,其 INC 指令就不会出错。他回复:“Java 吗?”我构造了一个例子:
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 的执行结果是符合预期的(在自增自减上二者语义是相同的):
都做到这份上了,不妨将前置后置、自增自减都来试一遍。有兴趣的同学可以编译一下。最终确认了,无论是前置还是后置,无论是自增还是自减,微软出品的 C++ 编译器都能做出一锅大杂烩。
// 环境: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++ 编译器在自增自减上病的不轻啊。
从中抽个例子,和昨天的不同的例子,用后置自增吧:
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 = ++x
和 y = 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。可是点击进去后,页面非常吓人:
我就是想报告一个编译器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 可能不适应微软自家的菜肴,于是我换上了微软的亲女儿 Edge:
你是在逗我吗?
以下两位是我搜索“report a compiler bug to visual studio”看到的。
看到这里我心都凉了。