Sicher sein bei “unbekannter Bewertungsreihenfolge”

Lesezeit: 5 Minuten

Benutzer-Avatar
Wolf

Seit Version 1.80 sagt mir Cppcheck das

Ausdruck ‘msg[ipos++]=Prüfsumme(&msg[1],ipos-1)’ hängt von der Reihenfolge der Bewertung der Nebenwirkungen ab

in dieser Codefolge (vereinfacht, data ist eine Variable)

BYTE msg[MAX_MSG_SIZE];  // msg can be smaller, depending on data encoded
int ipos = 0;
msg[ipos++] = MSG_START;
ipos += encode(&msg[ipos], data);
msg[ipos++] = checksum(&msg[1], ipos-1);  // <---- Undefined Behaviour?
msg[ipos++] = MSG_END;   // increment ipos to the actual size of msg

und behandelt dies als Fehler, nicht als Portabilitätsproblem.

Es ist C-Code (eingebaut in ein C++-dominiertes Projekt), kompiliert mit einem C++98-kompatiblen Compiler und läuft mittlerweile seit Jahrzehnten wie erwartet. Cppcheck wird mit C++03, C89, Auto-Detect-Sprache ausgeführt.

Ich gestehe, dass der Code besser umgeschrieben werden sollte. Aber bevor ich das tue, versuche ich herauszufinden: Ist es wirklich abhängig von der Auswertungsreihenfolge? So wie ich es verstehe, wird zuerst der rechte Operand ausgewertet (muss vor dem Aufruf), dann erfolgt die Zuweisung (to msg[ipos]) mit der Erhöhung von ipos zuletzt getan.

Liege ich mit dieser Annahme falsch, oder ist es nur ein falsch positives Ergebnis?

  • Hilfe. “Es gibt kein Konzept der Links-nach-Rechts- oder Rechts-nach-Links-Auswertung in C”

    – BiagioF

    29. August 2017 um 11:27 Uhr


  • Kannst du nicht einfach schreiben msg[ipos] = checksum(&msg[1], ipos++)?

    – Erich W

    29. August 2017 um 11:27 Uhr

  • @ErikW Das leidet unter dem gleichen Bestellproblem.

    – molbnilo

    29. August 2017 um 11:51 Uhr

  • „läuft mittlerweile seit Jahrzehnten wie erwartet“ – nichts für ungut, aber das klingt schon ein bisschen nach „funktioniert bei mir“. Unspezifiziertes und sogar undefiniertes Verhalten unterscheidet sich stark von zB einem Münzwurf. Ein Compiler kann immer das tun, was Sie wollen, ein anderer immer das, was Sie nicht wollen, und bei einem dritten kann das Verhalten tatsächlich sporadisch sein.

    – Arne Vogel

    29. August 2017 um 13:47 Uhr

  • @ArneVogel es funktioniert (nicht nur “für mich”), solange die Codebasis auf die aktuelle Plattform/Toolchain festgelegt ist. Seien Sie beruhigt: der Code Wille ändern, um eine Portierung zu ermöglichen. Es hier zu veröffentlichen, war die Lektion wert, die es erteilen kann.

    – Wolf

    29. August 2017 um 14:00 Uhr

Benutzer-Avatar
Johannes Zwinck

Dieser Code hängt tatsächlich auf eine nicht genau definierte Weise von der Auswertungsreihenfolge ab:

msg[ipos++] = checksum(&msg[1], ipos-1);

Konkret ist nicht angegeben, ob ipos++ wird davor oder danach erhöht ipos-1 ausgewertet wird. Dies liegt daran, dass es bei der keinen “Sequenzpunkt” gibt =nur am Ende des vollständigen Ausdrucks (the ;).

Der Funktionsaufruf ist ein Sequenzpunkt. Aber das garantiert nur ipos-1 geschieht vor dem Funktionsaufruf. Das garantiert es nicht ipos++ passiert danach.

Es scheint, dass der Code auf diese Weise umgeschrieben werden sollte:

msg[ipos] = checksum(&msg[1], ipos-1);
ipos++; // or ++ipos

  • @Wolf: Ja, der Funktionsaufruf ist ein Sequenzpunkt. Aber das garantiert nur ipos-1 geschieht vor dem Funktionsaufruf. Das garantiert es nicht ipos++ passiert danach.

    – Johannes Zwinck

    29. August 2017 um 11:34 Uhr

  • Ich würde Ihre augenöffnende Antwort gerne akzeptieren, aber könnten Sie auch das Detail über die undefinierte Anrufreihenfolge und die Berechnung des Zuordnungsziels aufnehmen. Meine Schuld war, anzunehmen, dass ich es hatte ipos-1 vor dem Call und Zuordnung nach dem Call würde reichen um hier sicher zu sein…

    – Wolf

    29. August 2017 um 11:41 Uhr

  • @Wolf dem Compiler steht es frei, die Speicheradresse zu berechnen, an der die Zuweisung geschrieben (und damit aktualisiert) wird ipos) vor der Auswertung der RHS.

    – Alnitak

    29. August 2017 um 11:48 Uhr

  • Ist es wirklich nur unspezifiziert oder eigentlich undefiniert? Beachten Sie, dass es vor C++11 ist.

    – Deduplizierer

    29. August 2017 um 11:51 Uhr


  • Was die neueren Standards sagen, ist irrelevant, da sich die Frage explizit auf C89, C++98 und C++03 bezieht.

    – Ludin

    29. August 2017 um 11:59 Uhr

Benutzer-Avatar
Lundin

Das Reihenfolge der Auswertung der Operanden von = ist unspezifiziert. Der Code stützt sich also zunächst auf unspezifiziertes Verhalten.

Was den Fall noch schlimmer macht, ist das ipos wird zweimal im selben Ausdruck ohne Sequenzpunkt dazwischen für nicht zusammenhängende Zwecke verwendet – was zu undefiniertem Verhalten führt.

C99 6.5

Zwischen dem vorherigen und dem nächsten Sequenzpunkt soll der gespeicherte Wert eines Objekts höchstens einmal durch die Auswertung eines Ausdrucks modifiziert werden. Außerdem soll der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

Derselbe Text gilt für C90, C99, C++98 und C++03. In C11 und C++11 hat sich der Wortlaut geändert, aber die Bedeutung ist dieselbe. Es ist ein undefiniertes Verhalten, vor C++11.

Der Compiler muss keine Diagnose für undefiniertes Verhalten geben. Du hattest Glück, dass es so war. Es ist kein falsch positives Ergebnis – Ihr Code enthält einen schwerwiegenden Fehler, den ganzen Weg vom ursprünglichen C-Code.

  • Speziell in Bezug auf den Zuweisungsoperator sagt C99: “Der Nebeneffekt der Aktualisierung des gespeicherten Werts des linken Operanden soll zwischen dem vorherigen und dem nächsten Sequenzpunkt auftreten.” Das ist aber egal, denn der UB entsteht bei der Wertberechnung selbst.

    – Ludin

    29. August 2017 um 11:57 Uhr


  • Für das, was es wert ist, war es nicht der Compiler, der die Warnung ausgegeben hat – es war ein statisches Analysetool (cppcheck, das kostenlos und nicht sehr aufwendig, aber nützlich ist).

    – Johannes Zwinck

    29. August 2017 um 12:23 Uhr


  • @JohnZwinck gcc/g++ mit genügend -Wall- und -Wextra-Flags neigt auch dazu, vor diesem UB zu warnen. (Insbesondere -Wsequence-point.)

    – Ludin

    29. August 2017 um 12:49 Uhr

  • @Wolf Nein, es ist offensichtlich nicht verwandt. Weder der Leser noch der Compiler können sagen, was diese Funktion tut. Sie rufen eine Funktion auf, die dies tut <whatever> mit der Variablen als Parameter im selben Ausdruck, wenn Sie diese Variable erhöhen. Sie können nicht wissen, ob die Variable vor oder nach dem Funktionsaufruf erhöht wird. Die Variable wird nicht “gelesen, um den zu speichernden Wert zu bestimmen”. Dies bezieht sich auf Fälle wie i = i + 1; was gut definiert ist.

    – Ludin

    29. August 2017 um 13:24 Uhr


  • @Wolf Es wurde sogar in C ++ 11 geändert (aber nicht in C11). Ich habe die neuen C++-Standards verwechselt. Antwort korrigiert. Sieh dir das an: en.cppreference.com/w/cpp/language/eval_orderscrollen Sie nach unten zu “undefiniertes Verhalten”.

    – Ludin

    29. August 2017 um 14:27 Uhr


1283710cookie-checkSicher sein bei “unbekannter Bewertungsreihenfolge”

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy