web-dev-qa-db-de.com

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

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

Was ändert sich an Dingen wie

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

und

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

oder

f(g(),h(),j());
76
Johan Lundberg

Einige gängige Fälle, in denen die Auswertungsreihenfolge bisher nicht spezifiziert war, sind mit C++17 Angegeben und gültig. Einige undefinierte Verhaltensweisen sind jetzt nicht spezifiziert.

Was ist mit Dingen wie

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

war undefiniert, ist jetzt aber nicht spezifiziert. Insbesondere ist nicht angegeben, in welcher Reihenfolge jedes Argument für f im Verhältnis zu den anderen ausgewertet wird. i++ Kann vor i ausgewertet werden oder umgekehrt. Tatsächlich wird ein zweiter Aufruf möglicherweise in einer anderen Reihenfolge ausgewertet, obwohl er sich unter demselben Compiler befindet.

Die Auswertung jedes Arguments ist jedoch erforderlich vollständig auszuführen, mit allen Nebenwirkungen, bevor ein anderes Argument ausgeführt wird. Sie könnten also f(1, 1) (zweites Argument zuerst ausgewertet) oder f(1, 2) (erstes Argument zuerst ausgewertet) erhalten. Aber Sie werden niemals f(2, 2) oder etwas anderes dieser Art erhalten.

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

Wurde nicht angegeben, wird jedoch mit der Rangfolge der Operatoren kompatibel, sodass die erste Auswertung von f im Stream an erster Stelle steht. (Beispiele unten).

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

hat noch nicht spezifizierte Bewertungsreihenfolge von g, h, j. Beachten Sie, dass in den Regeln für getf()(g(),h(),j()) festgelegt ist, dass getf() vor g,h,j Ausgewertet wird.

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 Die C++ - Programmiersprache, 4. Ausgabe, Stroustrup und war bisher nicht spezifiziertes Verhalten, aber mit C++ 17 funktioniert es wie erwartet. Es gab ähnliche Probleme mit wiederverwendbaren 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 wie erhalten

play
no,and,Work,All,

anstatt

All,work,and,no,play

Beachten Sie, dass das oben Gesagte 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 eingehen würden.

Referenzen: Aus dem angenommenen Vorschlag :

Postfix-Ausdrücke werden von links nach rechts ausgewertet. Dies schließt Funktionsaufrufe und Elementauswahlausdrücke ein.

Zuweisungsausdrücke werden von rechts nach links ausgewertet. Dies schließt zusammengesetzte Zuweisungen ein.

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

  1. a.b.
  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, an dem ein überladener Operator beteiligt ist, wird durch die Reihenfolge bestimmt, die dem entsprechenden eingebauten Operator zugeordnet ist, nicht durch die Regeln für Funktionsaufrufe.

Anmerkung bearbeiten: Meine ursprüngliche Antwort hat a(b1, b2, b3) falsch interpretiert. Die Reihenfolge von b1, b2, b3 Ist noch nicht festgelegt. (Danke an @KABoissonneault, alle Kommentatoren.)

Allerdings (wie @Yakk betont) und dies ist wichtig: Auch wenn b1, b2, b3 Nicht-triviale Ausdrücke sind, wird jeder von ihnen vollständig ausgewertet und an den jeweiligen Funktionsparameter gebunden bevor die anderen ausgewertet werden. In der Norm heißt es so:

§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 und 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 und die Initialisierung selbst werden vor jeder mit der Initialisierung eines nachfolgenden Parameters verbundenen Wertberechnung und Nebenwirkung sequenziert.

Das Beispiel ist da. Es löst jahrzehntealte Probleme ( wie von Herb Sutter erklärt ) mit Ausnahme der Sicherheit, wo Dinge mögen

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 rohe Zeiger an seinen intelligenten Zeigerparameter gebunden wurde. edit: wie von T.C. Das Beispiel ist fehlerhaft, da die unique_ptr-Konstruktion aus dem rohen Zeiger explizit ist, was das Kompilieren verhindert.

Beachten Sie auch dieses klassische Frage (markiert [~ # ~] c [~ # ~], nicht C++):

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

ist noch undefiniert.

64
Johan Lundberg

Interleaving ist in C++ 17 verboten

In C++ 14 war Folgendes nicht sicher:

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 so ist (1), (3), (2), (4) eine vollkommen gültige Reihenfolge. Wenn diese Reihenfolge ausgewählt wurde und (3) ausgelöst wird, 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 das Verschachteln. Aus [intro.execution]:

Für jeden Funktionsaufruf F wird 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, 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: (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.

35
Barry

Ich habe einige Hinweise zur Reihenfolge der Ausdrucksbewertung gefunden:

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

    In C++ 17 wurden einige Auswertungsreihenfolgen für überladene Operatoren und Regeln für vollständige Argumente hinzugefügt. Es bleibt jedoch unbestimmt, welches Argument zuerst angesprochen wird. In C++ 17 wird jetzt angegeben , dass der Ausdruck, der angibt, was aufzurufen ist (der Code links vom (Funktionsaufruf) Vor den Argumenten wird das Argument, das zuerst ausgewertet wird, vollständig ausgewertet, bevor das nächste gestartet wird. Bei einer Objektmethode wird der Wert des Objekts ausgewertet, bevor die Argumente für die Methode ausgewertet werden.

  • 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 ( undefiniert )

  • 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 der damit verbundene Nebeneffekt des Postfix-Ausdrucks werden vor denen der Ausdrücke in der Ausdrucksliste sequenziert. Die Initialisierungen der deklarierten Parameter werden ohne Interleaving unbestimmt sequenziert .

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

6.8.1.8 Sequentielle Ausführung [intro.execution] Ein Ausdruck X wird als vor einem Ausdruck Y sequenziert bezeichnet, wenn jede Wertberechnung und jeder mit dem Ausdruck X verbundene Nebeneffekt vor jeder Wertberechnung und jedem Wert sequenziert wird Nebenwirkung im Zusammenhang mit dem Ausdruck Y.

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 auszuwertenden 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 verglich ich das entsprechende Verhalten in drei Compilern für 14 und 17 Standards. 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 (je konsistenter es klingt):

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