C ++ Kompilierungsfehler?

74

Ich habe folgenden Code:

#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;
}

Ich erwarte, dass es "0, 1, 2, 3" ausgibt und stoppt, aber es gibt eine endlose Reihe von "0, 1, 2, 3, 4, 5, ....." aus.

Es sieht so aus, als ob der Vergleich di<4nicht gut funktioniert und immer true zurückgibt.

Wenn ich nur ,delta=mc[di]auskommentiere, erhalte ich wie gewohnt "0, 1, 2, 3". Was ist das Problem mit der unschuldigen Aufgabe?

Ich verwende Ideone.com g ++ C ++ 14 mit der Option -O2 .

eivour
quelle
5
Ich habe es mit g ++ versucht und ohne Optimierungen funktioniert es gut. Mit -O3 gibt es das vom OP erwähnte Verhalten. Mit -O1 ist es ok.
Chris Card
11
Klingt nach aggressiver Schleifenoptimierung aufgrund eines undefinierten Verhaltens wie in diesem Fall
Shafik Yaghmour
Obwohl der Code undefiniertes Verhalten hervorruft, ist die Optimierung ziemlich aggressiv und wie ich beobachte, hilft es nicht, dass gcc nicht konsequent eine Warnung für diese Optimierung bereitstellen kann.
Shafik Yaghmour
2
@Shafik Yaghmour Der Grund, warum GCC keine Warnung mit dem gibt, cout << diist wahrscheinlich, dass der Stream-Einfügeoperator für Komplex die Adresse dian einen "undurchsichtigen" Code weitergibt (oder dass der Stream-Einfügeoperator für Komplex selbst undurchsichtig ist - was mich überraschen würde obwohl). Und je nachdem, was dieser "undurchsichtige" Code tut, könnte das Verhalten des Programms immer noch gut definiert sein. Ich sage nicht, dass es in diesem Fall unmöglich wäre, eine Warnung ohne zu viele falsch positive (oder sogar falsch positive) Ergebnisse abzugeben. Nur dass es ziemlich schwer wäre.
Paul Groke
@ShafikYaghmour Ich verwende Ideone.com g ++ C ++ 14 mit der Option -O2. (Danke, ich habe es in meine Frage aufgenommen.)
eivour

Antworten:

108

Dies ist auf undefiniertes Verhalten zurückzuführen. Sie greifen mcbei der letzten Iteration Ihrer Schleife außerhalb der Grenzen auf das Array zu . Einige Compiler führen möglicherweise eine aggressive Schleifenoptimierung unter der Annahme durch, dass kein undefiniertes Verhalten vorliegt. Die Logik wäre wie folgt:

  • Der Zugriff mcaußerhalb der Grenzen ist ein undefiniertes Verhalten
  • Nehmen Sie kein undefiniertes Verhalten an
  • Daher di < 4ist immer wahr, da sonst mc[di]undefiniertes Verhalten aufrufen würde

gcc mit aktivierter Optimierung und Verwendung des -fno-aggressive-loop-optimizationsFlags verschwindet das Verhalten der Endlosschleife ( siehe live ). Während ein Live-Beispiel mit Optimierung, aber ohne -fno-aggressive-Schleifenoptimierungen, das von Ihnen beobachtete Endlosschleifenverhalten zeigt.

Ein Godbolt-Live-Beispiel des Codes zeigt, dass die di < 4Prüfung entfernt und durch ein bedingungsloses jmp ersetzt wurde:

jmp .L6

Dies ist fast identisch mit dem in GCC vor 4.8 Breaks Broken SPEC 2006 Benchmarks beschriebenen Fall . Die Kommentare zu diesem Artikel sind ausgezeichnet und es lohnt sich zu lesen. Es wird darauf hingewiesen, dass Clang den Fall in dem Artikel -fsanitize=undefinederfasst hat, mit dem ich ihn für diesen Fall nicht reproduzieren kann, aber mit gcc -fsanitize=undefined( siehe live ). Der wahrscheinlich berüchtigtste Fehler bei einem Optimierer, der auf undefiniertes Verhalten schließen lässt, ist das Entfernen der Nullzeigerprüfung des Linux-Kernels .

Obwohl dies eine aggressive Optimierung ist, ist es wichtig zu beachten, dass, wie der C ++ - Standard sagt, undefiniertes Verhalten ist:

Verhalten, für das diese Internationale Norm keine Anforderungen stellt

Was im Wesentlichen bedeutet, dass alles möglich ist und es bemerkt ( Hervorhebung von mir ):

[...] Zulässiges undefiniertes Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer dokumentierten, für die Umgebung charakteristischen Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe einer Diagnosemeldung). [...]

Um eine Warnung von gcc zu erhalten, müssen wir die coutAußenseite der Schleife verschieben und dann sehen wir die folgende Warnung ( sehen Sie sie live ):

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

Dies hätte wahrscheinlich ausgereicht, um das OP mit genügend Informationen zu versorgen, um herauszufinden, was vor sich ging. Solche Inkonsistenzen sind typisch für die Verhaltensweisen, die wir bei undefiniertem Verhalten beobachten können. Um ein besseres Verständnis dafür zu erhalten, warum solche Warnungen angesichts undefinierten Verhaltens inkonsistent sein können, warum können Sie bei der Optimierung aufgrund undefinierten Verhaltens nicht warnen? ist eine gute Lektüre.

Hinweis, -fno-aggressive-loop-optimizationsist in den Versionshinweisen zu gcc 4.8 dokumentiert .

Shafik Yaghmour
quelle
25
Dies. Bei undefiniertem Verhalten geht es nicht (nur) um Abstürze. Es geht um Annahmen, und wenn Sie gegen die Annahmen des Compilers verstoßen (wie in der Sprachspezifikation definiert), sind alle Wetten ungültig ...
Matthieu M.
@MatthieuM. In der Tat kommen wir immer wieder auf dieses Thema zurück. Einige Zitate in meiner Antwort hier sind ebenfalls relevant.
Shafik Yaghmour
Was für ein Idiot von mir, ich habe nicht bemerkt, dass ich auf mc zugegriffen habe [4];) Ich habe Ideone.com verwendet, also würde ich sowieso keine Warnung bekommen. Das nächste Mal werde ich einen Editor verwenden, der mich
warnt,
@eivour Es kann hilfreich sein, einen Link zu Ihrem Online-Beispiel bereitzustellen. In diesem Fall wird jedes Mal, wenn Sie ein Beispiel in ideone ausführen, eine URL zu diesem Beispiel bereitgestellt. Ich persönlich bevorzuge Coliru oder Wandbox, die beide einen Share-Button bieten.
Shafik Yaghmour
gcc 4.9.3 erzeugt immer noch keine Warnung: g++ -Waggressive-loop-optimizations -Wall -Wextra -O3 test.cpp- Laut dieser Seite sollte der Compiler eine Warnung anzeigen: gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
Peter VARGA
38

Da Sie inkrementieren, dibevor Sie es zum Indizieren verwenden mc, verweisen Sie beim vierten Mal durch die Schleife auf mc [4], das sich hinter dem Ende Ihres Arrays befindet, was wiederum zu störendem Verhalten führen kann.

Logicrat
quelle
Ignoriere meinen letzten Kommentar, der möglicherweise ein Problem mit ideone.com war, das meinen Code nach der Bearbeitung nicht richtig ausführt. Ein weiterer Test hat funktioniert: ideone.com/vLtvcy
Interjay
2
Die Verwendung von entweder di++,delta=mc[di-1]oder delta=mc[di],di++behebt das Problem. Sieht aus wie Logicrat ist richtig.
KompjoeFriek
2
Vielleicht gibt es einen Fehler nach dem anderen, und eivour wollte delta=mc[di++]nicht delta=mc[++di]alle mcWerte verwenden?
Toby Speight
5

Dies liegt daran, dass di ++ beim letzten Durchlauf der Schleife ausgeführt wird.

Zum Beispiel;

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)

Sie greifen auf mc [] zu, wenn di == 4 ist. Dies ist also ein Problem außerhalb der Grenzen, das möglicherweise einen Teil des Stapels zerstört und die Variable di beschädigt.

Eine Lösung wäre:

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

Du hast das:

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

Versuchen Sie stattdessen Folgendes:

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

BEARBEITEN:

Um zu klären, was los ist, brechen wir die Iteration Ihrer For-Schleife auf:

1. Iteration: Anfangs ist di auf 0 gesetzt. Vergleichsprüfung: Ist di kleiner als 4? Ja, okay, fahren Sie fort. Inkrementiere di um 1. Jetzt ist di = 1. Nimm das "n-te" Element von mc [] und setze es auf Delta. Dieses Mal greifen wir zum 2. Element, da dieser indizierte Wert 1 und nicht 0 ist. Führen Sie schließlich die Codeblöcke in der for-Schleife aus.

2. Iteration: Jetzt ist di auf 1 gesetzt. Vergleichsprüfung: Ist di kleiner als 4? Ja und weiter. Inkrementiere di um 1. Jetzt ist di = 2. Nimm das "n-te" Element von mc [] und setze es auf Delta. Dieses Mal greifen wir zum 3. Element, da dieser indizierte Wert 2 ist. Führen Sie schließlich die Codeblöcke in der for-Schleife aus.

3. Iteration: Jetzt ist di auf 2 gesetzt. Vergleichsprüfung: Ist di kleiner als 4? Ja und weiter. Inkrementiere di um 1. Jetzt ist di = 3. Nimm das "n-te" Element von mc [] und setze es auf Delta. Dieses Mal greifen wir zum 4. Element, da dieser indizierte Wert 3 ist. Führen Sie schließlich die Codeblöcke in der for-Schleife aus.

4. Iteration: Jetzt ist di auf 3 gesetzt. Vergleichsprüfung: Ist di kleiner als 4? Ja und weiter. Inkrementiere di um 1. Jetzt ist di = 4. (Kannst du sehen, wohin das führt?) Nimm das "n-te" Element von mc [] und setze es auf Delta. Dieses Mal greifen wir zum 5. Element, da dieser indizierte Wert 4 ist. Oh, wir haben ein Problem; Unsere Array-Größe beträgt nur 4. Delta hat jetzt Müll und dies ist undefiniertes Verhalten oder Korruption. Führen Sie abschließend die Codeblöcke in der for-Schleife mit "Garbage Delta" aus.

5. Iteration. Jetzt ist di auf 4 gesetzt. Vergleichsprüfung: Ist di kleiner als 4? Nein, aus der Schleife ausbrechen.

Beschädigung durch Überschreiten der Grenzen des zusammenhängenden Speichers (Array).

Francis Cugler
quelle