Hat dieser Code aus Abschnitt 4.3.6 der 4. Ausgabe von „The C ++ Programming Language“ ein genau definiertes Verhalten?

94

In Bjarne Stroustrups The C ++ Programming Language 4. Ausgabe, Abschnitt 36.3.6 STL-ähnliche Operationen, wird der folgende Code als Beispiel für die Verkettung verwendet :

void f2()
{
    std::string s = "but I have heard it works even if you don't believe in it" ;
    s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
        .replace( s.find( " don't" ), 6, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

Die Zusicherung schlägt in gcc( live sehen ) und Visual Studio( live sehen ) fehl, aber sie schlägt nicht fehl, wenn Clang verwendet wird ( live sehen ).

Warum bekomme ich unterschiedliche Ergebnisse? Bewerten einige dieser Compiler den Verkettungsausdruck falsch oder weist dieser Code irgendeine Form von nicht spezifiziertem oder undefiniertem Verhalten auf ?

Shafik Yaghmour
quelle
Besser:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt
20
Fehler beiseite, bin ich der einzige, der denkt, dass so hässlicher Code nicht in dem Buch sein sollte?
Karoly Horvath
5
@KarolyHorvath Beachten Sie, dass cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)nur unwesentlich weniger hässlich ist.
Oktalist
1
@Oktalist: :) Zumindest habe ich dort die Absicht. Es lehrt die argumentabhängige Namenssuche und Operatorsyntax gleichzeitig in einem knappen Format ... und es vermittelt nicht den Eindruck, dass Sie tatsächlich solchen Code schreiben sollten.
Karoly Horvath

Antworten:

104

Der Code zeigt aufgrund der nicht spezifizierten Reihenfolge der Auswertung von Unterausdrücken ein nicht spezifiziertes Verhalten, obwohl er kein undefiniertes Verhalten hervorruft, da alle Nebenwirkungen innerhalb von Funktionen ausgeführt werden, was in diesem Fall eine Sequenzierungsbeziehung zwischen den Nebenwirkungen einführt .

Dieses Beispiel wird im Vorschlag N4228 erwähnt: Verfeinern der Ausdrucksauswertungsreihenfolge für idiomatisches C ++ , in dem Folgendes zum Code in der Frage angegeben wird:

[...] Dieser Code wird von C ++ Experten weltweit und veröffentlicht (die Programmiersprache C ++, 4 prüft th Edition.) Doch seine Anfälligkeit für nicht spezifizierte Reihenfolge der Auswertung wurde durch ein Werkzeug erst vor kurzem entdeckt [.. .]

Einzelheiten

Für viele mag es offensichtlich sein, dass Argumente für Funktionen eine nicht spezifizierte Bewertungsreihenfolge haben, aber es ist wahrscheinlich nicht so offensichtlich, wie dieses Verhalten mit verketteten Funktionsaufrufen interagiert. Es war mir nicht klar, als ich diesen Fall zum ersten Mal analysierte, und anscheinend auch nicht allen Experten .

Auf den ersten Blick mag es so aussehen, replaceals müssten die entsprechenden Funktionsargumentgruppen auch als Gruppen von links nach rechts ausgewertet werden , da jede von links nach rechts ausgewertet werden muss.

Dies ist falsch, Funktionsargumente haben eine nicht spezifizierte Auswertungsreihenfolge, obwohl das Verketten von Funktionsaufrufen eine Auswertungsreihenfolge von links nach rechts für jeden Funktionsaufruf einführt. Die Argumente jedes Funktionsaufrufs werden nur in Bezug auf den Elementfunktionsaufruf, zu dem sie gehören, vorher sequenziert von. Dies wirkt sich insbesondere auf folgende Aufrufe aus:

s.find( "even" )

und:

s.find( " don't" )

die unbestimmt sequenziert sind in Bezug auf:

s.replace(0, 4, "" )

Die beiden findAufrufe könnten vor oder nach dem ausgewertet werden replace, was wichtig ist, da es eine Nebenwirkung hat s, die das Ergebnis von findverändert und die Länge von ändert s. Je nachdem, wann dies in replaceBezug auf die beiden findAufrufe ausgewertet wird, unterscheidet sich das Ergebnis.

Wenn wir uns den Verkettungsausdruck ansehen und die Bewertungsreihenfolge einiger Unterausdrücke untersuchen:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

und:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

Beachten Sie, dass wir die Tatsache ignorieren, dass 4und 7weiter in weitere Unterausdrücke unterteilt werden können. So:

  • Awird sequenziert, bevor Bsequenziert wird, bevor vorher Csequenziert wirdD
  • 1zu 9werden indeterminately in Bezug auf andere Unterausdrücke mit einigen der Ausnahmen unter sequenziert aufgelistet
    • 1zu 3werden sequenziert vorB
    • 4zu 6werden sequenziert vorC
    • 7zu 9werden sequenziert vorD

Der Schlüssel zu diesem Problem ist:

  • 4in 9Bezug auf unbestimmt sequenziert werdenB

Die mögliche Reihenfolge der Auswahl der Bewertung für 4und 7in Bezug auf Berklärt den Unterschied in den Ergebnissen zwischen clangund gccbei der Bewertung f2(). In meinen Tests clangauswertet , Bbevor die Bewertung 4und 7während gccauswertet nach. Wir können das folgende Testprogramm verwenden, um zu demonstrieren, was jeweils passiert:

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

Ergebnis für gcc( live sehen )

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

Ergebnis für clang( live sehen ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

Ergebnis für Visual Studio( live sehen ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

Details aus dem Standard

Wir wissen , dass die Bewertungen der Unterausdrücke sofern nicht anders angegeben sind unsequenced, diese aus dem ist Entwurf C ++ 11 - Standard Abschnitt 1.9 Programmausführung , die sagt:

Sofern nicht anders angegeben, werden Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke nicht sequenziert. [...]

und wir wissen, dass ein Funktionsaufruf eine vor der Beziehung der Funktionsaufrufe sequenzierte Postfix-Expression und Argumente in Bezug auf den Funktionskörper einführt, aus Abschnitt 1.9:

[...] Beim Aufrufen einer Funktion (unabhängig davon, ob die Funktion inline ist oder nicht) wird jede Wertberechnung und Nebenwirkung, die einem Argumentausdruck oder dem Postfix-Ausdruck zugeordnet ist, der die aufgerufene Funktion bezeichnet, vor der Ausführung jedes Ausdrucks oder jeder Anweisung sequenziert im Körper der aufgerufenen Funktion. [...]

Wir wissen auch, dass der Zugriff auf Klassenmitglieder und damit die Verkettung von links nach rechts ausgewertet wird, aus dem Abschnitt 5.2.5 Zugriff auf Klassenmitglieder, in dem Folgendes steht:

[...] Der Postfix-Ausdruck vor dem Punkt oder Pfeil wird ausgewertet. 64 Das Ergebnis dieser Auswertung bestimmt zusammen mit dem ID-Ausdruck das Ergebnis des gesamten Postfix-Ausdrucks.

Beachten Sie, dass in dem Fall, in dem der ID-Ausdruck eine nicht statische Elementfunktion ist, die Reihenfolge der Auswertung der Ausdrucksliste innerhalb von nicht angegeben wird, ()da dies ein separater Unterausdruck ist. Die relevante Grammatik aus 5.2 Postfix-Ausdrücken :

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

C ++ 17 Änderungen

Der Vorschlag p0145r3: Verfeinerung der Ausdrucksauswertungsreihenfolge für idiomatisches C ++ hat mehrere Änderungen vorgenommen. Einschließlich Änderungen, die dem Code ein genau festgelegtes Verhalten verleihen, indem die Reihenfolge der Bewertungsregeln für Postfix-Ausdrücke und deren Ausdrucksliste verstärkt wird .

[Ausdruck] p5 sagt:

Der Postfix-Ausdruck wird vor jedem Ausdruck in der Ausdrucksliste und vor jedem Standardargument sequenziert . Die Initialisierung eines Parameters, einschließlich aller zugehörigen Wertberechnungen und Nebenwirkungen, wird in Bezug auf die eines anderen Parameters unbestimmt sequenziert. [Hinweis: Alle Nebenwirkungen von Argumentauswertungen werden vor Eingabe der Funktion sequenziert (siehe 4.6). —Ende Anmerkung] [Beispiel:

void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

- Beispiel beenden]

Shafik Yaghmour
quelle
7
Ich bin ein wenig überrascht zu sehen, dass "viele Experten" das Problem übersehen haben. Es ist bekannt, dass die Auswertung des Postfix-Ausdrucks eines Funktionsaufrufs nicht sequenziert wird, bevor die Argumente ausgewertet werden (in allen Versionen von C und C ++).
MM
@ShafikYaghmour Die Funktionsaufrufe sind unbestimmt in Bezug aufeinander und auf alles andere sequenziert, mit Ausnahme der zuvor sequenzierten Beziehungen. Jedoch Auswertung von 1, 2, 3, 5, 6, 8, 9, "even", "don't"und das mehrere Instanzen ssind unsequenced relativ zueinander.
TC
4
@TC nein ist es nicht (so entsteht dieser "Bug"). Zum Beispiel foo().func( bar() )könnte es foo()entweder vor oder nach dem Anruf anrufen bar(). Der Postfix-Ausdruck ist foo().func. Die Argumente und der Postfix-Ausdruck werden vor dem Hauptteil von sequenziert func(), jedoch relativ zueinander nicht sequenziert.
MM
@ MattMcNabb Ah, richtig, ich habe falsch verstanden. Sie sprechen eher vom Postfix-Ausdruck selbst als vom Aufruf. Ja, das stimmt, sie sind nicht sequenziert (es sei denn, es gilt natürlich eine andere Regel).
TC
6
Es gibt auch den Faktor, dass man davon ausgeht, dass Code in einem B.Stroustrup-Buch korrekt ist, sonst hätte es sicherlich schon jemand bemerkt! (verwandt; SO-Benutzer finden immer noch neue Fehler in K & R)
MM
4

Dies soll Informationen zu diesem Thema in Bezug auf C ++ 17 hinzufügen. Der Vorschlag ( Refining Expression Evaluation Order für Idiomatic C ++ Revision 2 ) zur C++17Behebung des Problems unter Berufung auf den obigen Code war ein Muster.

Wie vorgeschlagen, habe ich relevante Informationen aus dem Vorschlag hinzugefügt und zitiert (hebt meine hervor):

Die Reihenfolge der Ausdrucksbewertung, wie sie derzeit im Standard festgelegt ist, untergräbt Ratschläge, gängige Programmiersprachen oder die relative Sicherheit von Standardbibliothekseinrichtungen. Die Fallen sind nicht nur für Anfänger oder den sorglosen Programmierer. Sie betreffen uns alle wahllos, auch wenn wir die Regeln kennen.

Betrachten Sie das folgende Programmfragment:

void f()
{
  std::string s = "but I have heard it works even if you don't believe in it"
  s.replace(0, 4, "").replace(s.find("even"), 4, "only")
      .replace(s.find(" don't"), 6, "");
  assert(s == "I have heard it works only if you believe in it");
}

Die Behauptung soll das beabsichtigte Ergebnis des Programmierers validieren. Es verwendet die "Verkettung" von Elementfunktionsaufrufen, eine übliche Standardpraxis. Dieser Code wurde von C ++ - Experten weltweit überprüft und veröffentlicht (The C ++ Programming Language, 4. Ausgabe). Seine Anfälligkeit für eine nicht spezifizierte Reihenfolge der Auswertung wurde jedoch erst kürzlich von einem Tool entdeckt.

Das Papier schlug vor, die Vorregel C++17für die Reihenfolge der Expressionsbewertung zu ändern, die von Cmehr als drei Jahrzehnten beeinflusst wurde und seit mehr als drei Jahrzehnten besteht. Es wurde vorgeschlagen, dass die Sprache zeitgenössische Redewendungen garantiert oder "Fallen und Quellen für dunkle, schwer zu findende Fehler" riskiert , wie dies bei dem obigen Codebeispiel der Fall war.

Der Vorschlag für C++17ist zu verlangen , dass jeder Ausdruck eine wohldefinierte Auswerteauftrag hat :

  • Postfix-Ausdrücke werden von links nach rechts ausgewertet. Dies umfasst Funktionsaufrufe und Mitgliederauswahlausdrücke.
  • Zuweisungsausdrücke werden von rechts nach links ausgewertet. Dies schließt zusammengesetzte Zuordnungen ein.
  • Operanden zum Verschieben von Operatoren werden von links nach rechts ausgewertet.
  • Die Reihenfolge der Auswertung eines Ausdrucks, an dem ein überladener Operator beteiligt ist, wird durch die Reihenfolge bestimmt, die dem entsprechenden integrierten Operator zugeordnet ist, nicht durch die Regeln für Funktionsaufrufe.

Der obige Code wird erfolgreich mit GCC 7.1.1und kompiliert Clang 4.0.0.

ricky m
quelle