C ++ - Ausführungsreihenfolge in der Methodenverkettung

108

Die Ausgabe dieses Programms:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Ist:

method 1
method 2:0

Warum ist nunicht 1, wenn meth2()startet?

Moises Viñas
quelle
41
@MartinBonner: Auch wenn ich die Antwort weiß, ich würde es nicht „offensichtlich“ in jedem Sinne des Wortes , und selbst wenn es war, wäre das kein anständiger Grund Drive-by - Downvote sein. Enttäuschend!
Leichtigkeitsrennen im Orbit
4
Dies erhalten Sie, wenn Sie Ihre Argumente ändern. Funktionen, die ihre Argumente ändern, sind schwerer zu lesen, ihre Auswirkungen sind für den nächsten Programmierer, der am Code arbeitet, unerwartet und führen zu solchen Überraschungen. Ich empfehle dringend, keine Parameter außer dem Invocant zu ändern. Das Ändern des Invokanten wäre hier kein Problem, da die zweite Methode für das Ergebnis der ersten aufgerufen wird und die Auswirkungen darauf geordnet sind. Es gibt immer noch Fälle, in denen dies nicht der Fall wäre.
Jan Hudec
@JanHudec Genau deshalb legt die funktionale Programmierung großen Wert auf Funktionsreinheit.
Pharap
2
Als Beispiel wird eine Stack-basierte würde Aufrufkonvention wahrscheinlich Push bevorzugen nu, &nuund cauf den Stapel in dieser Reihenfolge, dann invoke meth1, drücken Sie das Ergebnis auf den Stapel, dann invoke meth2, während eine registerbasierten Aufrufkonvention wollen , würde Laden cund &nuin Register, aufrufen meth1, nuin ein Register laden , dann aufrufen meth2.
Neil

Antworten:

66

Weil die Bewertungsreihenfolge nicht angegeben ist.

Sie sehen nuin mainder Auswertung, 0bevor überhaupt meth1aufgerufen wird. Dies ist das Problem bei der Verkettung. Ich rate davon ab.

Machen Sie einfach ein schönes, einfaches, klares, leicht zu lesendes und leicht verständliches Programm:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Leichtigkeitsrennen im Orbit
quelle
14
Es besteht die Möglichkeit, dass ein Vorschlag zur Klärung der Evaluierungsreihenfolge in einigen Fällen , der dieses Problem behebt, für C ++ 17
Revolver_Ocelot
7
Ich mag Methodenverkettungen (z. B. <<für die Ausgabe und "Objekt-Builder" für komplexe Objekte mit zu vielen Argumenten für die Konstruktoren - aber sie mischen sich sehr schlecht mit Ausgabeargumenten.
Martin Bonner unterstützt Monica
34
Verstehe ich das richtig Auswertungsreihenfolge von meth1und meth2ist definiert, aber die Auswertung der Parameter für meth2kann erfolgen, bevor meth1...?
Roddy
7
Die Methodenverkettung ist in Ordnung, solange die Methoden sinnvoll sind und nur den Invokanten modifizieren (für den die Effekte gut geordnet sind, da die zweite Methode für das Ergebnis der ersten aufgerufen wird).
Jan Hudec
4
Es ist logisch, wenn Sie darüber nachdenken. Es funktioniert wiemeth2(meth1(c, &nu), nu)
BartekChom
29

Ich denke, dieser Teil des Standardentwurfs bezüglich der Reihenfolge der Bewertung ist relevant:

1.9 Programmausführung

...

  1. Sofern nicht anders angegeben, werden Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke nicht sequenziert. Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert. Wenn eine Nebenwirkung auf ein Skalarobjekt in Bezug auf eine andere Nebenwirkung auf dasselbe Skalarobjekt oder eine Wertberechnung unter Verwendung des Werts desselben Skalarobjekts nicht sequenziert wird und sie möglicherweise nicht gleichzeitig auftreten, ist das Verhalten undefiniert

und auch:

5.2.2 Funktionsaufruf

...

  1. [Hinweis: Die Auswertungen des Postfix-Ausdrucks und der Argumente sind relativ zueinander nicht sequenziert. Alle Nebenwirkungen von Argumentauswertungen werden vor Eingabe der Funktion sequenziert - Endnote]

Überlegen Sie sich also für Ihre Zeile c.meth1(&nu).meth2(nu);, was im Operator in Bezug auf den Funktionsaufrufoperator für den letzten Aufruf von geschieht meth2, damit wir die Aufteilung in den Postfix-Ausdruck und das Postfix-Argument deutlich sehen nu:

operator()(c.meth1(&nu).meth2, nu);

Die Auswertungen des Postfix-Ausdrucks und des Arguments für den endgültigen Funktionsaufruf (dh des Postfix-Ausdrucks c.meth1(&nu).meth2und nu) sind gemäß der obigen Funktionsaufrufregel relativ zueinander nicht sequenziert . Daher ist der Nebeneffekt der Berechnung des Postfix-Ausdrucks auf das arSkalarobjekt relativ zur Argumentauswertung von nuvor dem meth2Funktionsaufruf nicht sequenziert . Nach der obigen Programmausführungsregel ist dies ein undefiniertes Verhalten.

Mit anderen Worten, der Compiler muss das nuArgument für den meth2Aufruf nach dem meth1Aufruf nicht auswerten. Es ist frei, keine Nebenwirkungen anzunehmen, die sich meth1auf die nuAuswertung auswirken .

Der oben erzeugte Assembler-Code enthält die folgende Sequenz in der mainFunktion:

  1. Die Variable nuwird auf dem Stapel zugewiesen und mit 0 initialisiert.
  2. Ein Register ( ebxin meinem Fall) erhält eine Kopie des Wertes vonnu
  3. Die Adressen von nuund cwerden in Parameterregister geladen
  4. meth1 wird genannt
  5. Das Rückgabewert - Register und der zuvor zwischengespeicherte Wert des nuin dem ebxRegister werden in Parameterregister geladen
  6. meth2 wird genannt

Entscheidend ist, dass der Compiler in Schritt 5 oben zulässt, dass der zwischengespeicherte Wert nuvon Schritt 2 im Funktionsaufruf an wiederverwendet wird meth2. Hier wird die Möglichkeit außer Acht gelassen, die nudurch den Aufruf von meth1"undefiniertes Verhalten" in Aktion geändert wurde .

HINWEIS: Diese Antwort hat sich im Wesentlichen von ihrer ursprünglichen Form geändert. Meine anfängliche Erklärung in Bezug auf die Nebenwirkungen der Operandenberechnung, die nicht vor dem letzten Funktionsaufruf sequenziert wurden, war falsch, weil sie es sind. Das Problem ist die Tatsache, dass die Berechnung der Operanden selbst unbestimmt sequenziert ist.

Smeeheey
quelle
2
Das ist falsch. Funktionsaufrufe werden mit anderen Auswertungen in der aufrufenden Funktion unbestimmt sequenziert (es sei denn, es wird anderweitig eine Einschränkung für die Sequenzierung vorher festgelegt). sie verschachteln nicht.
TC
1
@TC - Ich habe nie etwas über die Verschachtelung von Funktionsaufrufen gesagt. Ich habe nur auf Nebenwirkungen von Betreibern hingewiesen. Wenn Sie sich den oben erzeugten Assembly-Code ansehen, werden Sie sehen, dass er meth1zuvor ausgeführt wurde meth2, aber der Parameter für meth2ist ein Wert von, der nuvor dem Aufruf von in einem Register zwischengespeichert wurde meth1- dh der Compiler hat die möglichen Nebenwirkungen ignoriert, d. H. im Einklang mit meiner Antwort.
Smeeheey
1
Sie behaupten genau, dass - "seine Nebenwirkung (dh das Einstellen des Wertes von ar) nicht garantiert vor dem Aufruf sequenziert werden kann". Die Auswertung des Postfix-Ausdrucks in einem Funktionsaufruf ( dh c.meth1(&nu).meth2) und die Auswertung des Arguments für diesen Aufruf ( nu) sind im Allgemeinen nicht sequenziert, aber 1) ihre Nebenwirkungen werden alle vor dem Eintritt in meth2und 2) sequenziert, da c.meth1(&nu)es sich um einen Funktionsaufruf handelt wird es mit der Auswertung von unbestimmt sequenziert nu. Im Innern meth2, wenn es irgendwie einen Zeiger auf die Variable in erhalten main, wäre es immer zu sehen , 1.
TC
2
"Es ist jedoch nicht garantiert, dass der Nebeneffekt der Berechnung der Operanden (dh das Einstellen des Wertes von ar) vor irgendetwas (gemäß 2) oben sequenziert wird." Es ist absolut garantiert, dass es vor dem Aufruf von sequenziert wird meth2, wie in Punkt 3 der von Ihnen zitierten Referenzseite angegeben (die Sie auch nicht richtig zitiert haben).
TC
1
Sie haben etwas falsch gemacht und es noch schlimmer gemacht. Hier gibt es absolut kein undefiniertes Verhalten. Lesen Sie weiter [intro.execution] / 15, nach dem Beispiel.
TC
9

Im C ++ - Standard von 1998, Abschnitt 5, Abs. 4

Sofern nicht anders angegeben, ist die Reihenfolge der Auswertung von Operanden einzelner Operatoren und Unterausdrücke einzelner Ausdrücke sowie die Reihenfolge, in der Nebenwirkungen auftreten, nicht angegeben. Zwischen dem vorherigen und dem nächsten Sequenzpunkt soll der gespeicherte Wert eines Skalarobjekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden. Darüber hinaus darf nur auf den vorherigen Wert zugegriffen werden, um den zu speichernden Wert zu bestimmen. Die Anforderungen dieses Absatzes müssen für jede zulässige Reihenfolge der Unterausdrücke eines vollständigen Ausdrucks erfüllt sein. Andernfalls ist das Verhalten undefiniert.

(Ich habe einen Verweis auf Fußnote 53 weggelassen, der für diese Frage nicht relevant ist.)

&nuMuss im Wesentlichen vor dem Anruf ausgewertet c1::meth1()werden und numuss vor dem Anruf ausgewertet werden c1::meth2(). Es gibt jedoch keine Anforderung, nudie zuvor ausgewertet werden muss &nu(z. B. ist es zulässig, dass nuzuerst ausgewertet &nuwird und dann c1::meth1()aufgerufen wird - was möglicherweise Ihr Compiler tut). Es ist daher nicht garantiert, dass der Ausdruck *ar = 1in c1::meth1()ausgewertet wird, bevor nuin main()ausgewertet wird, um an übergeben zu werden c1::meth2().

Spätere C ++ - Standards (die ich derzeit nicht auf dem PC habe, den ich heute Abend verwende) haben im Wesentlichen dieselbe Klausel.

Peter
quelle
7

Ich denke, beim Kompilieren wurden die Parameter an sie übergeben, bevor die Funktionen meth1 und meth2 wirklich aufgerufen werden. Ich meine, wenn Sie "c.meth1 (& nu) .meth2 (nu);" Der Wert nu = 0 wurde an meth2 übergeben, daher spielt es keine Rolle, ob "nu" später geändert wird.

Sie können dies versuchen:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

es wird die Antwort bekommen, die Sie wollen

T-Shirt Heiliger
quelle