C++编译错误?

75

我有以下代码:

#include <iostream>
#include <complex>
using namespace std;

int main() {
    complex<int> delta;
    complex<int> mc[4] = {0};

    for(int di = 0; di < 4; di++, delta = mc[di]) {
        cout << di << endl;
    }

    return 0;
}

我期望它输出"0,1,2,3"并停止,但是它却输出了一个无尽的序列"0,1,2,3,4,5,......"

看起来比较运算符di<4不能良好地工作,并且总是返回true。

如果我只是注释掉,delta=mc[di],我会正常得到"0,1,2,3"。这个简单的赋值语句有什么问题吗?

我正在使用Ideone.com g++ C++14和-O2选项。


5
我已经尝试使用g++进行编译,在没有优化的情况下它可以正常工作。使用-O3时会出现OP提到的问题,但使用-O1则没有问题。 - Chris Card
11
听起来像是由于未定义的行为所导致的激进的循环优化,例如这个案例 - Shafik Yaghmour
尽管代码确实会调用未定义的行为,但优化非常激进,我观察到gcc无法始终为此优化提供警告,因此并没有帮助。 - Shafik Yaghmour
2
@Shafik Yaghmour GCC没有在cout << di中提供警告的原因可能是,对于复数的流插入运算符将di的地址传递给某些“不透明”的代码(或者复数的流插入运算符本身就是不透明的 - 尽管这让我感到惊讶)。根据那个“不透明”代码所做的事情,程序的行为仍然可以被定义。我并不是说在这种情况下不可能提供一个警告而没有太多的误报(甚至没有任何误报)。只是这将是相当困难的。 - Paul Groke
@ShafikYaghmour 我不会收到警告,因为我使用的是Ideone,并且编译成功了。下次我会找一个更好的编辑器:) - eivour
显示剩余5条评论
4个回答

110
这是由于未定义的行为,您在循环的最后一个迭代中越界访问了数组mc。一些编译器可能会围绕没有未定义的行为的假设进行积极的循环优化。逻辑类似于以下内容:
  • 越界访问mc是未定义的行为
  • 假设没有未定义的行为
  • 因此,di < 4始终为真,否则mc [di]将调用未定义的行为
使用打开优化并使用-fno-aggressive-loop-optimizations标志的gcc会导致无限循环行为消失(查看实时演示)。而使用优化但不使用-fno-aggressive-loop-optimizations的实时示例会出现您观察到的无限循环行为。
代码的godbolt实时示例显示di < 4检查被删除并替换为无条件jmp。
jmp .L6

这与GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks中所述的情况几乎相同。该文章的评论非常好,值得一读。它指出,clang使用-fsanitize=undefined捕获了文章中的案例,但我无法为此案例复现,但是gcc使用-fsanitize=undefined可以(现场查看)。围绕优化器对未定义行为进行推断的最臭名昭着的错误可能是Linux内核空指针检查删除

尽管这是一种激进的优化方式,但重要的是要注意,正如C++标准所说,未定义行为是:

本国际标准不强制执行的行为

这意味着基本上任何事情都有可能发生,它还指出(我强调):

[...] 允许的未定义行为范围从完全忽略情况 带有不可预测的结果, 到在翻译或程序执行期间以环境的特性的记录方式行为(无论是否发出诊断消息),到终止翻译或执行(伴随着发出诊断消息)[...]

为了从gcc获得警告,我们需要将cout移动到循环外,然后我们会看到以下警告 (现场查看):

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
     for(di=0; di<4;di++,delta=mc[di]){ }
     ^

这已经足以为原作者提供足够的信息来找出问题所在。类似这样的不一致性通常是我们在处理未定义行为时看到的行为类型。要更好地理解为什么这种警告在面对未定义行为时可能是不一致的,可以阅读为什么无法在基于未定义行为进行优化时发出警告?

请注意,-fno-aggressive-loop-optimizationsgcc 4.8 发布说明中有记录。


27
这句话的意思是:未定义行为不仅是指程序崩溃,它还涉及到假设。如果你违反了编译器根据语言规范所做出的假设,那么一切皆有可能会出现问题... - Matthieu M.
@MatthieuM。确实,我们一直回到那个主题,我在这里的回答中的一些引用也是相关的。 - Shafik Yaghmour
真是我的疏忽,我没有注意到我正在访问 mc[4] ;) 我正在使用 Ideone.com,所以我不会得到任何警告。 下次我会使用一个即使编译成功也能给我警告的编辑器 :) - eivour
@eivour,提供一个在线示例的链接可能很有帮助,在这种情况下,我认为每次在ideone中运行一个示例都会提供一个url。个人而言,我更喜欢使用[Coliru](http://coliru.stacked-crooked.com/)或 [Wandbox](http://melpon.org/wandbox/),它们都提供共享按钮。 - Shafik Yaghmour
使用编译语言与动态语言相比的一个重要优势是编译器可以为您捕获错误,因此编译器决定静默省略代码而不是警告未定义行为是一个糟糕的选择。这个问题是编译器本可以帮助用户的完美例子,但它选择让用户感到困惑。 - jtchitty
显示剩余4条评论

38

由于您在使用 mc 的索引 di 之前对其进行了递增,因此在循环的第四次迭代中,您将引用 mc [4],这超出了数组末尾,可能会导致问题。


忽略我上一条评论,那可能是ideone.com在编辑后没有正确地重新运行我的代码的问题。另一个测试确实有效:http://ideone.com/vLtvcy - interjay
2
使用di ++,delta = mc [di-1]delta = mc [di],di ++也可以解决问题。看起来Logicrat是正确的。 - KompjoeFriek
2
也许存在一个偏移量错误,eivour可能意味着delta=mc[di++]而不是delta=mc[++di],以使用所有的mc值? - Toby Speight

5
您有以下内容:
for(int di=0; di<4; di++, delta=mc[di]) {
  cout<<di<<endl;
}

可以尝试使用以下方法:

for(int di=0; di<4; delta=mc[di++]) {
   cout<<di<<endl;
}

编辑:

为了澄清正在发生的事情,让我们分解一下您的for循环迭代:

第一次迭代:最初将di设置为0。 比较检查:di是否小于4?是的,继续。 将di增加1。现在di = 1。获取mc []的“nth”元素并将其设置为delta。这次我们获取的是第二个元素,因为此索引值为1而不是0。最后执行for循环中的代码块/ s。

第二次迭代:现在将di设置为1。 比较检查:di是否小于4?是的,请继续。 将di增加1。现在di = 2。获取mc []的“nth”元素并将其设置为delta。这次我们获取的是第三个元素,因为此索引值为2。最后执行for循环中的代码块/ s。

第三次迭代:现在将di设置为2。 比较检查:di是否小于4?是的,请继续。 将di增加1。现在di = 3。获取mc []的“nth”元素并将其设置为delta。这次我们获取的是第四个元素,因为此索引值为3。 最后执行for循环中的代码块/ s。

第四次迭代:现在将di设置为3。 比较检查:di是否小于4?是的,请继续。 将di增加1。现在di = 4。(你看到这是怎么回事了吗?)获取mc []的“nth”元素并将其设置为delta。这次我们获取的是第五个元素,因为此索引值为4。糟糕,我们有一个问题;我们的数组大小仅为4。Delta现在具有垃圾,这是未定义的行为或损坏。最后使用“垃圾三角洲”执行for循环中的代码块/ s。

第五次迭代。现在将di设置为4。 比较检查:di是否小于4?不,退出循环。

通过超出连续内存(数组)的范围而导致的损坏。


5

这是因为 di++ 是在循环的最后一次执行的。

举个例子;

int di = 0;
for(; di < 4; di++);
// after the loop di == 4
// (inside the loop we see 0,1,2,3)
// (inside the for statement, after di++, we see 1,2,3,4)

当 di == 4 时,您正在访问 mc[],因此这是一个越界问题,可能会破坏部分堆栈并损坏变量 di。

解决方案如下:

for(int di = 0; di < 4; di++) {
    cout << di << endl;
    delta = mc[di];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接