Was sind die von C++17 eingeführten Garantien für die Bewertungsreihenfolge?

Lesezeit: 12 Minuten

Was sind die von C17 eingefuhrten Garantien fur die Bewertungsreihenfolge
Johann Lundberg

Welche Auswirkungen hat die Stimmabgabe Garantien für die C++17-Evaluierungsreihenfolge (P0145) auf typischen C++-Code?

Was ändert es an Dingen wie den folgenden?

i = 1;
f(i++, i)

und

std::cout << f() << f() << f();

oder

f(g(), h(), j());

  • Bezieht sich auf die Reihenfolge der Auswertung der Zuweisungsanweisung in C++ und hat dieser Code aus „The C++ Programming Language“, 4. Auflage, Abschnitt 36.3.6, ein wohldefiniertes Verhalten? die beide vom Papier abgedeckt sind. Der erste könnte in Ihrer Antwort unten ein nettes zusätzliches Beispiel abgeben.

    – Shafik Yaghmour

    15. Mai 2017 um 17:57 Uhr

  • Auch etwas verwandt: c++17-Evaluierungsreihenfolge mit Funktionen zum Überladen von Operatoren.

    – dfrib

    29. September 2017 um 6:54 Uhr

Was sind die von C17 eingefuhrten Garantien fur die Bewertungsreihenfolge
Johann Lundberg

Einige häufige Fälle, in denen die Bewertungsreihenfolge bisher lautete nicht spezifiziertsind angegeben und gültig mit C++17. Einige undefinierte Verhaltensweisen sind jetzt stattdessen unspezifiziert.

i = 1;
f(i++, i)

war undefiniert, ist aber jetzt nicht spezifiziert. Was insbesondere nicht angegeben ist, ist die Reihenfolge, in der die einzelnen Argumente ausgeführt werden f wird relativ zu den anderen bewertet. i++ kann vorher ausgewertet werden i, oder umgekehrt. Tatsächlich könnte es einen zweiten Aufruf in einer anderen Reihenfolge auswerten, obwohl er sich unter demselben Compiler befindet.

Allerdings ist die Bewertung jedes Arguments erforderlich vollständig auszuführen, mit allen Nebeneffekten, bevor irgendein anderes Argument ausgeführt wird. Also könntest du bekommen f(1, 1) (zweites Argument wird zuerst ausgewertet) oder f(1, 2) (erstes Argument wird zuerst ausgewertet). Aber du wirst es nie bekommen f(2, 2) oder irgendetwas anderes dieser Art.

std::cout << f() << f() << f();

wurde nicht angegeben, wird aber mit der Operatorpriorität kompatibel, sodass die erste Auswertung von f kommt zuerst im Stream (Beispiele unten).

f(g(), h(), j());

hat immer noch eine nicht spezifizierte Auswertungsreihenfolge von g, h und j. Beachten Sie das für getf()(g(),h(),j())das sagen die Regeln getf() wird vorher ausgewertet g, h, j.

Beachten Sie auch folgendes Beispiel aus dem Vorschlagstext:

 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, "");

Das Beispiel stammt aus Die Programmiersprache C++, 4. Auflage, Stroustrup, und war früher ein nicht spezifiziertes Verhalten, aber mit C++17 funktioniert es wie erwartet. Es gab ähnliche Probleme mit fortsetzbaren Funktionen (.then( . . . )).

Betrachten Sie als weiteres Beispiel Folgendes:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // Pre-C++17 version:
        auto word = words[i] + (i+1==words.size()?"\n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"\n":",");

    }
};

int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

Mit C++14 und davor können (und werden) wir Ergebnisse wie bekommen

play
no,and,Work,All,

anstatt

All,work,and,no,play

Beachten Sie, dass das Obige tatsächlich dasselbe ist wie

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

Trotzdem gab es vor C++17 keine Garantie dafür, dass die ersten Aufrufe zuerst in den Stream kommen würden.

Referenzen: Von der akzeptierte Vorschlag:

Postfix-Ausdrücke werden von links nach rechts ausgewertet. Dazu gehören Funktionsaufrufe und Elementauswahlausdrücke.

Zuweisungsausdrücke werden von rechts nach links ausgewertet. Dazu gehören zusammengesetzte Zuweisungen.

Operanden zum Verschieben von Operatoren werden von links nach rechts ausgewertet. Zusammenfassend werden die folgenden Ausdrücke in der Reihenfolge a, dann b, dann c, dann d ausgewertet:

  1. ab
  2. a-> b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @= a
  6. ein[b]
  7. ein << b
  8. a >> b

Darüber hinaus schlagen wir die folgende zusätzliche Regel vor: Die Reihenfolge der Auswertung eines Ausdrucks mit einem überladenen Operator wird durch die Reihenfolge bestimmt, die dem entsprechenden eingebauten Operator zugeordnet ist, nicht den Regeln für Funktionsaufrufe.

Notiz bearbeiten: Meine ursprüngliche Antwort falsch interpretiert a(b1, b2, b3). Die Reihenfolge von b1, b2, b3 ist noch unbestimmt. (Danke @KABoissonneault, alle Kommentatoren.)

Allerdings (wie @Yakk betont) und das ist wichtig: Auch wenn b1, b2, b3 sind nicht-triviale Ausdrücke, jeder von ihnen wird vollständig ausgewertet und an den jeweiligen Funktionsparameter gebunden bevor mit der Auswertung der anderen begonnen wird. Die Norm sagt das so:

§5.2.2 – Funktionsaufruf 5.2.2.4:

. . . Der Postfix-Ausdruck wird vor jedem Ausdruck in der Ausdrucksliste und jedem Standardargument sequenziert. Jede Wertberechnung und jeder Nebeneffekt, der mit der Initialisierung eines Parameters verbunden ist, und die Initialisierung selbst werden vor jeder Wertberechnung und jedem Nebeneffekt, der mit der Initialisierung eines beliebigen nachfolgenden Parameters verbunden ist, sequenziert.

Allerdings fehlt einer dieser neuen Sätze den GitHub-Entwurf:

Jede Wertberechnung und jeder Nebeneffekt, der mit der Initialisierung eines Parameters verbunden ist, und die Initialisierung selbst werden vor jeder Wertberechnung und jedem Nebeneffekt, der mit der Initialisierung eines beliebigen nachfolgenden Parameters verbunden ist, sequenziert.

Das Beispiel ist dort. Es löst ein jahrzehntealtes Problem (wie von Herb Sutter erklärt) mit Ausnahme der Sicherheit, wo Dinge wie

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(), get_raw_a());

wenn einer der anrufe auslaufen würde get_raw_a() auslösen würde, bevor der andere Rohzeiger an seinen Smart-Pointer-Parameter gebunden wurde.

Wie von TC betont, ist das Beispiel fehlerhaft, da die unique_ptr-Konstruktion aus dem Rohzeiger explizit ist und verhindert, dass dieser kompiliert wird.*

Beachten Sie auch diese klassische Frage (getaggt Cnicht C++):

int x=0;
x++ + ++x;

ist noch undefiniert.

  • „Ein zweiter untergeordneter Vorschlag ersetzt die Auswertungsreihenfolge von Funktionsaufrufen wie folgt: Die Funktion wird vor allen ihren Argumenten ausgewertet, aber jedes Paar von Argumenten (aus der Argumentliste) wird unbestimmt sequenziert; was bedeutet, dass eines vor dem anderen ausgewertet wird, außer dem Die Reihenfolge ist nicht festgelegt; es ist garantiert, dass die Funktion vor den Argumenten ausgewertet wird. Dies spiegelt einen Vorschlag einiger Mitglieder der Kernarbeitsgruppe wider.”

    – Yakk – Adam Nevraumont

    21. Juli 2016 um 12:05 Uhr

  • Ich bekomme diesen Eindruck aus dem Papier, das besagt, dass “die folgenden Ausdrücke in der Reihenfolge ausgewertet werden adann bdann cdann d“ und dann zeigen a(b1, b2, b3)was darauf hindeutet, dass alle b Ausdrücke werden nicht unbedingt in beliebiger Reihenfolge ausgewertet (andernfalls wäre es a(b, c, d))

    – KABoissonneault

    21. Juli 2016 um 12:14 Uhr


  • @KABoissoneault, Sie haben Recht und ich habe die Antwort entsprechend aktualisiert. Auch alle: die Anführungszeichen sind Formularversion 3, die, soweit ich weiß, die gewählte Version ist.

    – Johan Lundberg

    21. Juli 2016 um 12:28 Uhr


  • @JohanLundberg Es gibt eine andere Sache aus der Zeitung, die ich für wichtig halte. a(b1()(), b2()()) kann bestellen b1()() und b2()() in beliebiger Reihenfolge, aber es kann nicht tun b1() dann b2()() dann b1()(): Es darf ihre Hinrichtungen nicht mehr verschachteln. Kurz gesagt, “8. ALTERNATE EVALUATION ORDER FOR FUNCTION CALLS” war Teil der genehmigten Änderung.

    – Yakk – Adam Nevraumont

    21. Juli 2016 um 13:40 Uhr


  • f(i++, i) war undefiniert. Es ist jetzt unbestimmt. Stroustrups Saitenbeispiel war wahrscheinlich nicht spezifiziert, nicht undefiniert. ` f(get_raw_a(),get_raw_a());` wird nicht kompiliert, da die relevanten unique_ptr Konstruktor ist explizit. Endlich, x++ + ++x ist undefiniert, Punkt.

    – TC

    21. Juli 2016 um 17:26 Uhr


Was sind die von C17 eingefuhrten Garantien fur die Bewertungsreihenfolge
Barry

Interleaving ist in C++17 verboten

In C++14 war Folgendes unsicher:

void foo(std::unique_ptr<A>, std::unique_ptr<B>);

foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

Hier finden während des Funktionsaufrufs vier Operationen statt

  1. new A
  2. unique_ptr<A> Konstrukteur
  3. new B
  4. unique_ptr<B> Konstrukteur

Die Reihenfolge dieser war völlig unspezifiziert, und daher ist eine vollkommen gültige Reihenfolge (1), (3), (2), (4). Wenn diese Reihenfolge ausgewählt wurde und (3) wirft, dann leckt der Speicher von (1) – wir haben (2) noch nicht ausgeführt, was das Leck verhindert hätte.


In C++17 verbieten die neuen Regeln Interleaving. Von [intro.execution]:

Für jeden Funktionsaufruf F, für jede Auswertung A, die innerhalb von F auftritt, und jede Auswertung B, die nicht innerhalb von F auftritt, aber im selben Thread und als Teil desselben Signalhandlers (falls vorhanden) ausgewertet wird, wird entweder A vor B sequenziert oder B wird vor A sequenziert.

Zu diesem Satz gibt es eine Fußnote, die lautet:

Mit anderen Worten, Funktionsausführungen verschachteln sich nicht.

Dies hinterlässt uns zwei gültige Reihenfolgen: (1), (2), (3), (4) oder (3), (4), (1), (2). Es ist nicht angegeben, welche Reihenfolge angenommen wird, aber beide sind sicher. Alle Anordnungen, bei denen (1) (3) beide vor (2) und (4) erfolgen, sind jetzt verboten.

  • Eine kleine Randbemerkung, aber das war einer der Gründe für boost::make_shared und später std::make_shared (ein anderer Grund sind weniger Zuweisungen + bessere Lokalität). Klingt so, als ob die Motivation für Ausnahmesicherheit/Ressourcenleck nicht mehr gilt. Siehe Codebeispiel 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Bearbeiten und stackoverflow.com/a/48844115 , herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers

    – Max Barraclough

    6. Juli 2018 um 9:16 Uhr


  • Ich frage mich, wie sich diese Änderung auf die Optimierung auswirkt. Der Compiler hat jetzt eine stark reduzierte Anzahl von Optionen zum Kombinieren und Verschachteln von CPU-Anweisungen im Zusammenhang mit der Berechnung von Argumenten, sodass dies zu einer schlechteren CPU-Auslastung führen kann.

    – Violette Giraffe

    22. August 2019 um 7:34 Uhr

  • Was ist mit dem Fall von obj.modify().f(obj.access()): Ist es wohldefiniert, wenn obj.modify() kommt davor oder danach obj.access()? (Es klingt wie zumindest in obj.modify().f(obj.access().foo()) alle obj.access().foo() würde eher “gemeinsam” passieren obj.modify() hinterher sequenziert wird obj.access() Vor .foo().)

    – Ben

    26. Oktober 2021 um 18:09 Uhr

  • @Ben Das wird von der Top-Antwort behandelt. Im a.b, a wird vorher ausgewertet b.

    – Barri

    26. Oktober 2021 um 20:00 Uhr

Ich habe einige Hinweise zur Reihenfolge der Ausdrucksauswertung gefunden:

  • Quick F: Warum hat C++ keine festgelegte Reihenfolge zum Auswerten von Funktionsargumenten?

    In C++17 wurden einige Garantien für die Bewertungsreihenfolge rund um überladene Operatoren und Regeln für vollständige Argumente hinzugefügt. Aber es bleibt unbestimmt, welches Argument zuerst kommt. In C++17 ist es jetzt spezifiziert dass der Ausdruck, der angibt, was aufgerufen werden soll (der Code links vom ( des Funktionsaufrufs), vor den Argumenten steht und das zuerst ausgewertete Argument vollständig ausgewertet wird, bevor das nächste gestartet wird, und im Fall einer Objektmethode Der Wert des Objekts wird vor den Argumenten der Methode ausgewertet.

  • Reihenfolge der Bewertung

    21) Jeder Ausdruck in einer durch Kommas getrennten Liste von Ausdrücken in einem in Klammern gesetzten Initialisierer wird wie für einen Funktionsaufruf ausgewertet (unbestimmt sequenziert)

  • Mehrdeutige Ausdrücke

    Die C++-Sprache garantiert nicht die Reihenfolge, in der Argumente für einen Funktionsaufruf ausgewertet werden.

Im P0145R3.Refining Expression Evaluation Order for Idiomatic C++ Ich habe gefunden:

Die Wertberechnung und der damit verbundene Seiteneffekt des Postfix-Ausdrucks werden vor denen der Ausdrücke in der Ausdrucksliste geordnet. Die Initialisierungen der deklarierten Parameter sind unbestimmt geordnet ohne Verschachtelung.

Aber ich habe es nicht im Standard gefunden, sondern im Standard habe ich gefunden:

6.8.1.8 Sequentielle Ausführung [intro.execution]

Ein Ausdruck X wird vor einem Ausdruck Y angeordnet, wenn jede Wertberechnung und jeder Nebeneffekt, der dem Ausdruck X zugeordnet ist, vor jeder Wertberechnung und jedem Nebeneffekt, der dem Ausdruck Y zugeordnet ist, angeordnet ist.

6.8.1.9 Sequentielle Ausführung [intro.execution]

Jede Wertberechnung und Nebenwirkung, die einem vollständigen Ausdruck zugeordnet sind, wird vor jeder Wertberechnung und Nebenwirkung, die dem nächsten zu bewertenden vollständigen Ausdruck zugeordnet sind, sequenziert.

7.6.19.1 Kommaoperator [expr.comma]

Ein durch ein Komma getrenntes Ausdruckspaar wird von links nach rechts ausgewertet;…

Also habe ich das entsprechende Verhalten in drei Compilern für 14- und 17-Standards verglichen. Der untersuchte Code lautet:

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

Ergebnisse (umso konsistenter ist Klang):

<style type="text/css">
  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }
  
  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }
  
  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }
  
  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
  
  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
</style>
<table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-fymr">C++14</th>
    <th class="tg-fymr">C++17</th>
  </tr>
  <tr>
    <td class="tg-fymr"><br>gcc 9.0.1<br></td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">clang 9</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">msvs 2017</td>
    <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
</table>

998510cookie-checkWas sind die von C++17 eingeführten Garantien für die Bewertungsreihenfolge?

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

Privacy policy