【问题标题】:Why does i += a[i]++ assign i before incrementing a[i] with Visual C++?为什么 i += a[i]++ 在使用 Visual C++ 递增 a[i] 之前分配 i?
【发布时间】:2017-12-07 09:01:50
【问题描述】:

问题

  • 这个例子是这个问题的最小重现,遇到了代码高尔夫尝试。我不提倡任何人使用这种风格。

  • 我知道我可能遇到了未定义的行为,我的问题是:为什么具体是这样,这对 C 和 C++ 都适用吗?

我有这个文件:

#include <stdio.h>
int main() {
    int a[]={1, 0}, i=0;
    i += a[i]++;
    printf("i=%d a=[%d, %d]\n", i, a[0], a[1]);
    return 0;
}

在 WSL 上的 Ubuntu 上使用 GCC,我得到了预期的 i=1 a=[2, 0],但使用 Visual C++,它是 i=1 a=[1, 1]

如果我这样做,问题就解决了:

delta = a[i]++;
i += delta;

虽然未指定赋值的评估顺序,但令我感到惊讶的是,a[i] 似乎在 i 的赋值之后再次评估了增量,。下面的反汇编似乎证实了这一点。

C 或 C++ 标准是否允许这样做?


背景

在 Windows 上构建命令行和输出:

>cl test.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.12.25830.2 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

test.c
Microsoft (R) Incremental Linker Version 14.12.25830.2
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:test.exe
test.obj

声明的反汇编(使用/Zi构建):

     4:     i += a[i]++;
00007FF6CBBA626C  movsxd      rax,dword ptr [i]  
00007FF6CBBA6271  mov         eax,dword ptr a[rax*4]  
00007FF6CBBA6275  mov         ecx,dword ptr [i]  
00007FF6CBBA6279  add         ecx,eax  
00007FF6CBBA627B  mov         eax,ecx  
00007FF6CBBA627D  mov         dword ptr [i],eax  
00007FF6CBBA6281  movsxd      rax,dword ptr [i]  
00007FF6CBBA6286  mov         eax,dword ptr a[rax*4]  
00007FF6CBBA628A  inc         eax  
00007FF6CBBA628C  movsxd      rcx,dword ptr [i]  
00007FF6CBBA6291  mov         dword ptr a[rcx*4],eax  

【问题讨论】:

  • 对标题中提出的问题的答案仍然是“因为这个语句没有定义的行为”。我真的不明白你想从这个问题中得到什么。此语句在 C 或 C++ 中没有定义的行为。句号。试图推理它的作用是没有用的,就像我们不试图从“这个陈述是错误的”这句话中理解一样。
  • @SijmenMulder:你打破的规则是“在一个无序的步骤中读取和写入同一个变量”。我们还能说什么?!
  • @SijmenMulder:C 和 C++ 都将其归为 UB,尽管标准引用了略微不同的原因,主要集中在 C++ 中尚未定义的排序点的重新定义上(尚未)将其纳入 C 标准。
  • 实际上我没有看到任何理由说代码未定义。它与i = i + a[i] 相同,这很好(增加a[0] 的无序副作用并不重要,因为这不会影响i)。在所有 C 和 C++ 版本中,右侧所有操作数的评估(但不是副作用)在写入左侧之前排序
  • 我投票决定重新打开,因为在我所有的努力中,我只能推断它是 MSVC 中的一个错误(据说它支持 C++11),因此不是一个骗子。 @OliverCharlesworth

标签: c++ c visual-c++


【解决方案1】:

代码在所有版本的 C 和 C++ 中定义良好。您在 MSVC 中看到了一个错误。

+= 运算符的定义是x += y 表示x = x + y,只是x 只计算一次。 (请注意,“评估”与“左值转换”不同)。

所以我们正在查看i = i + a[i]++

非正式地:在+ 的两个操作数都被计算并执行左值转换之前,无法计算存储在i 中的新值。因为我们需要左值转换的结果才能知道要存储的值。

术语“评估”左值意味着确定左值所指的内存位置。当i0 时评估a[i]++ 意味着确定我们指的是a[0]。然后左值转换检索a[0]的存储值,未排序的副作用是a[0]的存储值迟早会更新。

写信给a[0]i 无关,因此没有理由相信可能存在未定义的行为。


更正式地说:在 2011 年之前,C 和 C++ 标准使用了一个有点令人困惑的句子,表达了我的“非正式:”段落中的逻辑。如果表达式读取和写入i,那么只有当i 的读取对于确定要存储在i 中的新值都是必需的时,它才是明确定义的,因为这将保证读取都发生在它之前可以存储新值。

在 C11 和 C++11 中,语言发生了变化,但含义没有变化。 C11 6.5.16/3(赋值运算符)说:

更新左操作数的存储值的副作用是 在左右操作数的值计算之后排序。

毫无疑问,对i 的写入是在右侧对i 的所有读取之后排序的。

C++11 有类似的语言:

在所有情况下,赋值都是在左右操作数的值计算之后进行排序的,

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-26
    • 2020-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-06
    • 2014-02-22
    • 2017-06-19
    相关资源
    最近更新 更多