Welche Garantien für die Evaluierungsreihenfolge werden von C ++ 17 eingeführt?

90

Welche Auswirkungen haben die in C ++ 17 abgestimmten Garantien für die Evaluierungsreihenfolge (P0145) auf typischen C ++ - Code?

Was ändert sich an Dingen wie den folgenden?

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

und

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

oder

f(g(), h(), j());
Johan Lundberg
quelle
Bezogen auf die Reihenfolge der Auswertung der Zuweisungsanweisung in C ++ und Hat dieser Code aus Abschnitt 4.3.6 der 4. Ausgabe von „The C ++ Programming Language“ ein genau definiertes Verhalten? die beide von dem Papier abgedeckt werden. Der erste könnte ein schönes zusätzliches Beispiel in Ihrer Antwort unten machen.
Shafik Yaghmour

Antworten:

81

Einige häufige Fälle, in denen die Bewertungsreihenfolge bisher nicht spezifiziert wurde , sind spezifiziert und gültig mit C++17. Einige undefinierte Verhaltensweisen sind jetzt nicht mehr spezifiziert.

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

war undefiniert, ist aber jetzt nicht spezifiziert. Insbesondere wird nicht die Reihenfolge angegeben, in der jedes Argument frelativ zu den anderen bewertet wird. i++kann vorher ausgewertet iwerden oder umgekehrt. In der Tat kann ein zweiter Aufruf in einer anderen Reihenfolge ausgewertet werden, obwohl er sich unter demselben Compiler befindet.

Die Bewertung jedes Arguments muss jedoch vollständig mit allen Nebenwirkungen ausgeführt werden, bevor ein anderes Argument ausgeführt wird. So erhalten Sie möglicherweise f(1, 1)(zweites Argument zuerst ausgewertet) oder f(1, 2)(erstes Argument zuerst ausgewertet). Aber Sie werden nie f(2, 2)etwas anderes von dieser Art bekommen.

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

wurde nicht angegeben, wird jedoch mit der Priorität des Operators kompatibel, sodass die erste Auswertung von fim Stream an erster Stelle steht (Beispiele unten).

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

hat noch eine nicht spezifizierte Bewertungsreihenfolge von g, h und j. Beachten Sie, dass für getf()(g(),h(),j())die Regeln der Status angegeben getf()wird, der zuvor ausgewertet wird g, h, j.

Beachten Sie auch das folgende 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 der C ++ - Programmiersprache , 4. Ausgabe, Stroustrup, und war früher ein nicht spezifiziertes Verhalten, aber mit C ++ 17 funktioniert es wie erwartet. Es gab ähnliche Probleme mit wiederaufnehmbaren 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 früher können (und werden) wir Ergebnisse erhalten wie

play
no,and,Work,All,

anstatt

All,work,and,no,play

Beachten Sie, dass das oben Gesagte tatsächlich dasselbe ist wie

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

Vor C ++ 17 gab es jedoch keine Garantie dafür, dass die ersten Aufrufe zuerst in den Stream gelangen.

Referenzen: Aus dem angenommenen Vorschlag :

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. 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. a [b]
  7. a << 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 integrierten Operator zugeordnet ist, nicht durch die Regeln für Funktionsaufrufe.

Anmerkung bearbeiten: Meine ursprüngliche Antwort wurde falsch interpretiert a(b1, b2, b3). Die Reihenfolge der b1, b2, b3ist noch nicht spezifiziert. (Danke @KABoissonneault, alle Kommentatoren.)

Jedoch (wie @Yakk weist darauf hin) , und das ist wichtig: Auch wenn b1, b2, b3sind nicht-triviale Ausdrücke ist jeder von ihnen vollständig ausgewertet und an die jeweiligen Funktionsparameter gebunden , bevor die andere ausgewertet wird gestartet. Der Standard besagt dies folgendermaßen:

§5.2.2 - Funktionsaufruf 5.2.2.4:

. . . Der Postfix-Ausdruck wird vor jedem Ausdruck in der Ausdrucksliste und vor jedem Standardargument sequenziert. Jede mit der Initialisierung eines Parameters verbundene Wertberechnung und Nebenwirkung sowie die Initialisierung selbst werden vor jeder mit der Initialisierung eines nachfolgenden Parameters verbundenen Wertberechnung und Nebenwirkung sequenziert.

Einer dieser neuen Sätze fehlt jedoch im GitHub-Entwurf :

Jede mit der Initialisierung eines Parameters verbundene Wertberechnung und Nebenwirkung sowie die Initialisierung selbst werden vor jeder mit der Initialisierung eines nachfolgenden Parameters verbundenen Wertberechnung und Nebenwirkung sequenziert.

Das Beispiel ist da. 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());

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

Wie von TC hervorgehoben, ist das Beispiel fehlerhaft, da die Konstruktion von unique_ptr aus dem Rohzeiger explizit ist und dies das Kompilieren verhindert. *

Beachten Sie auch diese klassische Frage (markiert mit C , nicht mit C ++ ):

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

ist noch undefiniert.

Johan Lundberg
quelle
1
"Ein zweiter Nebenvorschlag ersetzt die Auswertungsreihenfolge von Funktionsaufrufen wie folgt: Die Funktion wird vor allen ihren Argumenten ausgewertet, aber jedes Argumentpaar (aus der Argumentliste) wird unbestimmt sequenziert, was bedeutet, dass eines vor dem anderen außer dem ausgewertet wird Die Reihenfolge ist nicht angegeben. Es wird garantiert, dass die Funktion vor den Argumenten bewertet wird. Dies spiegelt einen Vorschlag einiger Mitglieder der Kernarbeitsgruppe wider. "
Yakk - Adam Nevraumont
1
Ich bekomme diesen Eindruck von dem Papier, das sagt, dass "die folgenden Ausdrücke in der Reihenfolge ausgewertet werden a, dann b, dann c, dann d" und dann zeigt a(b1, b2, b3), was darauf hindeutet, dass nicht alle bAusdrücke notwendigerweise in irgendeiner Reihenfolge ausgewertet werden (andernfalls wäre es a(b, c, d))
KABoissonneault
1
@ KABoissoneault, Sie sind richtig und ich habe die Antwort entsprechend aktualisiert. Außerdem alle: Die Anführungszeichen stammen aus Version 3, die meines Wissens in der Version gewählt wurde.
Johan Lundberg
2
@JohanLundberg Es gibt noch etwas in der Zeitung, das ich für wichtig halte. a(b1()(), b2()())bestellen kann b1()()und b2()()in beliebiger Reihenfolge, aber es kann nicht tun , b1()dann b2()()dann b1()(): es nicht mehr kann Verschachtelung ihrer Ausführungen. Kurz gesagt, "8. ALTERNATE EVALUATION ORDER FOR FUNCTION CALLS" war Teil der genehmigten Änderung.
Yakk - Adam Nevraumont
3
f(i++, i)war undefiniert. Es ist jetzt nicht spezifiziert. Stroustrups String-Beispiel war wahrscheinlich nicht spezifiziert, nicht undefiniert. `f (get_raw_a (), get_raw_a ());` wird nicht kompiliert, da der relevante unique_ptrKonstruktor explizit ist. Schließlich x++ + ++xist undefiniert, Punkt.
TC
43

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

Während des Funktionsaufrufs finden hier 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 unbestimmt, und daher ist eine vollkommen gültige Reihenfolge (1), (3), (2), (4). Wenn diese Reihenfolge ausgewählt wurde und (3) ausgelöst wird, ist der Speicher von (1) undicht - wir haben (2) noch nicht ausgeführt, was das Leck verhindert hätte.


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

Für jeden Funktionsaufruf F, für jede Auswertung A, die innerhalb von F auftritt, und für jede Auswertung B, die nicht innerhalb von F auftritt, sondern auf demselben 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 miteinander.

Dies lässt uns zwei gültige Ordnungen übrig: (1), (2), (3), (4) oder (3), (4), (1), (2). Es ist nicht spezifiziert, welche Bestellung angenommen wird, aber beide sind sicher. Alle Bestellungen, bei denen (1) (3) beide vor (2) und (4) erfolgen, sind jetzt verboten.

Barry
quelle
1
Ein wenig beiseite, aber dies war einer der Gründe für boost :: make_shared und später std :: make_shared (ein anderer Grund sind weniger Zuordnungen + 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/… Edit and stackoverflow.com/a/48844115 , herbsutter.com/2013/05/29/gotw-89-solution- Smart-Pointer
Max Barraclough
1
Ich frage mich, wie sich diese Änderung auf die Optimierung auswirkt. Der Compiler hat jetzt die Anzahl der Optionen zum Kombinieren und Verschachteln von CPU-Anweisungen im Zusammenhang mit der Argumentberechnung erheblich reduziert, sodass dies zu einer schlechteren CPU-Auslastung führen kann.
Violette Giraffe
2

Ich habe einige Hinweise zur Reihenfolge der Ausdrucksbewertung gefunden:

  • Schnelle Frage: Warum hat c ++ keine festgelegte Reihenfolge für die Auswertung von Funktionsargumenten?

    Eine bestimmte Reihenfolge der Auswertung garantiert, dass überladene Operatoren und Regeln für vollständige Argumente in C ++ 17 hinzugefügt wurden. Es bleibt jedoch das, was zuerst argumentiert wird, nicht spezifiziert. In C ++ 17 wird jetzt angegeben, 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 vor dem nächsten ausgewertet wird gestartet, und im Fall einer Objektmethode wird der Wert des Objekts ausgewertet, bevor die Argumente für die Methode lauten.

  • Reihenfolge der Bewertung

    21) Jeder Ausdruck in einer durch Kommas getrennten Liste von Ausdrücken in einem Initialisierer in Klammern 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.

In P0145R3.Refining Expression Evaluation Order für Idiomatic C ++ habe ich gefunden:

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

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

6.8.1.8 Sequentielle Ausführung [intro.execution] Ein Ausdruck X wird vor einem Ausdruck Y sequenziert, wenn jede Wertberechnung und jeder dem Ausdruck X zugeordnete Nebeneffekt vor jeder Wertberechnung und jeder dem Ausdruck Y zugeordneten Nebenwirkung sequenziert wird .

6.8.1.9 Sequentielle Ausführung [intro.execution] Jede mit einem vollständigen Ausdruck verbundene Wertberechnung und Nebenwirkung wird vor jeder mit dem nächsten zu bewertenden vollständigen Ausdruck verbundenen Wertberechnung und Nebenwirkung sequenziert.

7.6.19.1 Kommaoperator [expr.comma] Ein durch ein Komma getrenntes Ausdruckspaar wird von links nach rechts ausgewertet; ...

Also habe ich das 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 (desto konsistenter ist das Klirren):

<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>

lvccgd
quelle