web-dev-qa-db-ja.com

C ++コンパイルのバグ?

私は次のコードを持っています:

#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オプションを使用しています。

74
eivour

これは未定義の動作によるもので、ループの最後の繰り返しで境界外の配列mcにアクセスしています。一部のコンパイラは、未定義の動作がないという仮定の周りで積極的なループ最適化を実行する場合があります。ロジックは次のようになります。

  • mcの範囲外へのアクセスは未定義の動作です
  • 未定義の動作を想定しない
  • したがって、di < 4は常に真です。そうでない場合、mc[di]は未定義の動作を呼び出します。

最適化を有効にしてgccを使用し、-fno-aggressive-loop-optimizationsフラグを使用すると、無限ループの動作が消滅します(see it live)。一方、 最適化あり-fno-aggressive-loop-optimizationsなしの実例 は、無限ループ動作を示します。

コードのゴッドボルトライブ例 は、di < 4チェックが削除され、無条件のjmpに置き換えられたことを示します。

jmp .L6

これは、 GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks で説明されているケースとほぼ同じです。この記事へのコメントは優れており、読む価値があります。この記事では、clangが-fsanitize=undefinedを使用して記事でケースをキャッチしたが、このケースでは再現できないが、-fsanitize=undefinedを使用してgccが(ライブを見る =)。おそらく、未定義の動作に関する推論を行うオプティマイザーに関する最も悪名高いバグは、 LinuxカーネルのNULLポインターチェックの削除 です。

これは積極的な最適化ですが、C++標準では未定義の動作は次のとおりであることに注意することが重要です。

この国際標準が要件を課していない行動

これは本質的に何でも可能であることを意味し、(emphasis mine):

[...]許容される未定義の動作は、状況を完全に無視する予測不能な結果から、環境に特有の文書化された方法での翻訳またはプログラム実行中の動作(または(診断メッセージの発行なし)、翻訳または実行の終了(診断メッセージの発行あり)。[...]

Gccから警告を取得するには、coutをループの外側に移動する必要があり、次の警告が表示されます(see it live):

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

これは、何が起こっているのかを把握するのに十分な情報をOPに提供するのにおそらく十分でした。このような不整合は、未定義の動作で見られる典型的な動作のタイプです。未定義の動作に直面してそのような警告が不整合になる理由をよりよく理解するには 未定義の動作に基づいて最適化するときに警告できないのはなぜですか? を読むとよいでしょう。

注、-fno-aggressive-loop-optimizationsgcc 4.8リリースノート に記載されています。

109
Shafik Yaghmour

diをインデックスするために使用する前にmcをインクリメントしているので、ループの4回目はmc [4]を参照します。面倒な行動につながるリード。

38
Logicrat

ループの最後の実行で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];
}
5
PaulHK

あなたはこれを持っています:

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[]の「n番目」の要素を取得し、それをデルタに設定します。今回は、このインデックス値が0ではなく1であるため、2番目の要素を取得します。最後にforループ内でコードブロックを実行します。

2回目の反復:diは1に設定されます。比較チェック:diは4未満ですか?はい、続行します。 diを1増やします。今度はdi = 2です。mc[]の「n番目」の要素を取得し、それをデルタに設定します。今回はこのインデックス値が2であるため、3番目の要素を取得します。最後にforループ内でコードブロックを実行します。

3回目の反復:diは2に設定されます。比較チェック:diは4未満ですか?はい、続行します。 diを1増やします。di = 3になります。mc[]の「n番目」の要素を取得し、それをデルタに設定します。今回は、このインデックス値が3であるため、4番目の要素を取得します。最後にforループ内でコードブロックを実行します。

4回目の反復:diは3に設定されます。比較チェック:diは4未満ですか?はい、続行します。 diを1増やします。di = 4になります(これがどこに行くのかわかりますか?)mc []の「n番目」の要素を取得し、それをデルタに設定します。今回は、このインデックス値が4であるため、5番目の要素を取得しています。配列のサイズは4のみです。デルタにはゴミがあり、これは未定義の動作または破損です。最後に、 "garbage delta"を使用してforループ内でコードブロックを実行します。

5回目の反復。 diは4に設定されます。比較チェック:diは4未満ですか?いいえ、ループから抜け出します。

連続したメモリ(配列)の境界を超えることによる破損。

4
Francis Cugler