C++ Reihenfolge der Bearbeitung der Funktionsparameter

scooter010

Commander Pro
Registriert
Sep. 2014
Beiträge
2.831
Ich bin grade an einer kleinen Übung und kann folgenden Zusammenhang nicht nachvollziehen.

Ich habe ein paar Funktionen, die ich unter Visual Studio Community Version 2019 erstellt habe:
C++:
double minus(double a, double b) {
    return a - b;
}
int rest(int a, int b) {
    if (b <= 0 || a < 0)
        std::_Xout_of_range("Operanden außerhalb des zulässigen Bereiches");
    return a % b;
}

int gv(int i, const (char*) message);
double gv(double d, const (char*) message);
/*
Beide gv() erzeugen eine Konselenabfrage (cin) und wandeln das Ergebnis entweder in einen int (stoi()) 
oder double (stod()) Rückagewert um. Variable i und d sind ungenutzt und dienen nur der 
"Überladbarkeit" der Funktion, in dem je nach gewünschten Antwortwert ein mit einem beliebigen 
Wert initialisierter int oder double mit übergeben wird.
*/

aufgerufen werden die Funktionen unterschiedlich.
C++:
double d = 0;
int i = 0;
//Mal so:
case '2':
            std::cout << "Differenz: " << minus(gv(d,"Minuend"), gv(d,"Substrahend")) << std::endl;
            error = 0;
            break;
//Anderer Code
//Und dann mal so
case '5':
            int f;
            try {
                f = rest(gv(i, "zu teilende Zahl"), gv(i, "Teiler"));
            }
            catch (const std::out_of_range & oor) {
                std::cerr << "Fehler " << oor.what() << std::endl;
                std::cout << "Program startet von vorne" << std::endl;
                error = 1;
                break;
            }
            std::cout << "Rest: " << f << std::endl;
            error = 0;
            break;

Erwartetes Verhalten:
Beim Aufruf der jeweiligen Funktion wird jeweis erst der am weitestens links stehende Parameter ermittelt und nach rechts abgearbeitet.

Beobachtetes Verhalten:
Im 2. case entspricht das beobachtete Verhalten dem erwartetem Verhalten.
Im 5. case weicht das Verhalten von der Erwartung ab, es wird zuerst der Rechte Funktionsparameter ermittelt und somit entsprechend der Funktion von gv() beim Nutzer abgefragt und anschließend der Linke Parameter

Fragestellung:
Warum tritt dieses Verhalten auf?
Kann ich irgendwie steuern, welcher Funktions-Parameter zuerst ermittelt werden soll, ohne die Ermittlung der Parameter in separate Variablen Zwischenspeichern zu müssen?
 
Nein, C++ gibt keine Garantien für die Auswertungsreihenfolge der Funktionsparameter. Hintergrund ist unter anderem den Compiler bei Optimierungen nicht zuweit einzuschränken.

Garantiert ist die Auswertungsreihenfolge nur bei den eingebauten Short Circuit Operatoren, also beispielsweise && und ||
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: wayne_757 und scooter010
Vermutung, (sicher bin ich nicht):
Bei std:cout und dem "<<"-Operator wird die Ausführungsreihenfolge von links nach rechts wahrscheinlich im Standard spezifiziert sein. Sonst würden diese Streams (und so sind die konzipiert: https://en.cppreference.com/w/cpp/io/cout ) nicht funktionieren. Der Output-Stream-Buffer wird initialisiert und alles, was reingeschoben wird, wird ausgegeben.

Über den Funktionsaufruf läuft aber der Optimizer des Compilers drüber. Beim Aufruf der Funktion müssen alle Parameter bekannt sein. Mir ist aber nicht bekannt, das eine Reihenfolge definiert wäre, in der die Parameter ausgewertet werden müssten. Da würde wenn im C++-Standard stehen, ein mehrere tausend Seiten Wälzer.
Wenn die Reihenfolge dem Compiler freigestellt ist, kann er den generierten Assembler sortieren wie er will, also auch von rechts nach links lesen. Oder von der Mitte nach außen bei mehr als 4 Parametern. Je mehr Freiheiten der Compiler/Optimizer hat, desto besser (=schneller, kleiner, effizienter) kann er den Maschinencode gestalten. Also gibt man so viele Freiheiten wie möglich.
 
  • Gefällt mir
Reaktionen: scooter010
Damit ist meine gv() Funktion per (fehlender) Definition absoluter Mist. Bzw. die angedachte Aufrufstruktur. Auch so lernt man...

Danke
 
Leider hast du den Output nicht gepostet, aber deiner Beschreibung nach folgere ich, dass es in etwa so aussieht?

Code:
Menüauswahl : 2

minuend : 5.123
subtrahend : 1.123
Differenz: 4


Menüauswahl : 5

Teiler : 8
zu teilende Zahl : 17
Rest : 1

Die Ausführungsreihenfolge der Parameter ist nicht zwingend von links nach rechts wie du dir das vorstellst. Ich verstehe aber vor allem nicht warum du dich dagegen sträubst, die Abfragen sequentiell abzufrühstücken und in einer Variable zwischenzuspeichern - Speicherplatz kann es ja nicht sein, da du ja einen Integer und einen Double als reines Unterscheidungskriterium für die Überladung definierst.

Mach die Abfragen einfach nacheinander, um die Ausführungsreihenfolge sicherzustellen, und pack das Ergebnis der Abfrage in eine Variable und gut ist.
 
  • Gefällt mir
Reaktionen: new Account() und scooter010
Auch in C++17 ist die Reihenfolge der Auswertung f(gv1(), gv2()) nicht definiert. Außer natürlich, dass f erst nach gv1 und gv2 ausgeführt wird.
das std::cout spielt hier so wie es da steht meines Erachtens nach keine Rolle.

Sprich, das kannst und solltest du so nicht schreiben, das Ergebnis kann sich auch je nach verwendetem Compiler unterscheiden.

Auch in Programmiersprachen, in denen die Reihenfolge definiert ist wird es meines Erachtens nach durchweg für schlechten Stil gehalten, derart Anweisungen zu verschachteln.
 
  • Gefällt mir
Reaktionen: new Account() und scooter010
@Rajin Entschuldige, ich hätte die Ausgabe mit posten sollen. Aber du hast es richtig erkannt, so sieht die Ausgabe aus.
Ich sträube mich nicht dagegen, das in eigene Variablen zu packen, weil es einem speziell Zweck (Speicherplatz) dienen würde. Ich finde es nur "eleganter" wenn man den Code kompakter hält. Wenn man eine Variable im Code nicht verwendet, wozu dann erst definieren?
Gut, hier gibt es nun einen Grund.
 
scooter010 schrieb:
Ich finde es nur "eleganter" wenn man den Code kompakter hält. Wenn man eine Variable im Code nicht verwendet, wozu dann erst definieren?
Dein Verständnis von Eleganz wird sich mit der Zeit vermutlich noch anpassen.
Verständlichkeit / Lesbarkeit wird allgemein für einen wesentlichen Faktor von Eleganz gehalten beim Programmieren.
Solche Verschachtelungen sind deutlich schwerer zu lesen.
 
  • Gefällt mir
Reaktionen: mental.dIseASe und Raijin
Wenn man dazu schöne, generische Programmierung anwenden will, würde man wahrscheinlich den Weg über "auto" für die Variablen d und i benutzen, die Funktionen gv(), minus() und rest() als Template implementieren und über den switch nur noch Addition und Division unterscheiden (obwohl auch da zu prüfen wäre, ob man das nicht noch abstrahieren könnte.)

Wenn man mehr in Algorithmen denkt (und weniger in Spaghetti-Code bzw. "Ich-skripte-mir-was-ich-haben-will") und dem Compiler noch mehr Freiheiten lässt, könnte man an der Stelle den Code wahrscheinlich kürzer gestalten, gleichzeitig schneller und außerdem noch flexibler, weil der Compiler genau den Code erzeugen kann/muss, den er wirklich braucht.
 
@BeBur Da hast du vermutlich vollkommen recht. Verbesserung ist der Zweck meiner Übungen.

@Fortatus
Ich werde jetzt erstmal auf den Weihnachtsmarkt und danach als Steigerung, die Operanten und Berechnung in eine Berechnung-Klasse hauen. Mal sehen, ob ich aus GV() noch eine Eingabeklasse mache, mit getInt() und getdouble() als Methoden.

Edith: Es macht natürlich überhaupt keinen Sinn, bei einem Taschenrechner auf der Kommandozeile Nacht int und Double zu unterscheiden. Es ist einfach als Übung gedacht, um auch mit überladenen Funktionen sauber zu arbeiten und von Anfang an möglichst OO zh arbeiten, für was Anderes als einen Taschenrechner macht die Unterscheidung vielleicht schon Sinn.
 
Quellcode wird nicht übersichtlicher und eleganter, nur weil er kürzer ist. Quellcode sollte kurz, prägnant und gut strukturiert sein und vor allem keinen Raum für Interpretationen bieten. Wird der Code zu lang, muss man eben mehr Kapseln, mit generischen Funktionen/Klassen arbeiten, o.ä.
Ich hätte beispielsweise die einzelnen cases in Funktionen ausgelagert. Dann steht da nur noch das hier:

Code:
case '2':
            error =Minus();
            break;
case '5':
            error =Rest();
            break;

Das lässt wenig Spielraum für Interpretationen, weil so auch ein fremder Entwickler mit einem Blick sieht was mutmaßlich passiert, wenn man die 2 gedrückt hat - ohne erstmal nach oben zur Ausgabe des Menüs scrollen zu müssen. Klar kann man jeden case auch erstmal mit einem Kommentar versehen, der dann darüber Aufschluss gibt welchem Zweck der case dient, aber mit sprechenden (Funktions)Namen wird der Kommentar letztendlich überflüssig.

Bei der Ausführungsreihenfolge muss man eben auch immer im Hinterkopf behalten, dass der Compiler kein Mensch ist und nach anderen Kriterien vorgeht. Du hast ja gemerkt, dass beide Aufrufe für den Compiler durchaus einen Unterschied machen. Theoretisch kann es passieren, dass bei der nächsten Änderung am Code und dem daraus resultierenden Kompilat kann sogar plötzlich der umgekehrte Fall eintreten, weil die Optimierung des Compilers es diesmal anders optimieren will und case '2' vermeintlich falschherum interpretiert wird.

Eleganz und Lesbarkeit erkennt man daher in der Regel daran, dass auch ein fremder Entwickler ohne Kenntnis des Projekts den Code lesen, verstehen und nachvollziehen kann.


scooter010 schrieb:
Mal sehen, ob ich aus GV() noch eine Eingabeklasse mache, mit getInt() und getdouble() als Methoden.
Das ist doch gar keine so schlechte Idee. Kapselung ist immer gut und vor allem sind das dann sprechende Namen ;)
 
scooter010 schrieb:
und von Anfang an möglichst OO zh arbeiten
Wichtig (für die Zukunft als Profi): Alle Programmier-Paradigmen (statisch vs dynamisch typisiert; Objektorientiert vs Spaghetti-Code) sind Mittel zum Zweck und nie Selbstzweck. (Zumindest sollte es so sein.)
All diese Ideen sollen dazu dienen, die Softwareentwicklung zu vereinfachen, indem sie dem Programmierer eine Leitschnur zur Verfügung stellen.
D.h. indem Moment, wenn ein starres Festhalten an diesen Dingen zur Last wird (z.B. bezüglich Lesbarkeit, Performance, Code-Struktur), sollte man sie über Bord werfen (können). Bei manchen Programmiersprachen (C++ z.B.) ist das leicht, bei manchen (z.B. altem Fortran, Java) ist das schwerer.
In C++ kann man von direktem Assembler, über Spaghetti-Code und Objektorientierung bis zur Meta-/Template-Programmierung alles benutzen. Ein Profi weiß, wann er welches Werkzeug nimmt. Ein Anfänger ist zu Anfang eher überfordert und später ist man vllt. übermotiviert und hat den berühmten Schuss in den Fuss (und das Bein ist ab.)

Auch heute haben händische Assembler-Optimierungen in spezieller Software (Kernel, Videoencoder, etc) noch eine Bedeutung. Und mancher "optimierter" Software würde ein bisschen mehr Abstraktion gut tun.
 
  • Gefällt mir
Reaktionen: Raijin
@Fortatus du hast natürlich Recht. Um die Paradigmen nutzen, einschätzen und auch ausschließen zu können, muss ich sie auch erstmal kennen. Spaghetti und Funktional geht einigermaßen, OO noch nicht.

Wenn ich die Grundlagen anhand von C++ verstanden habe von C++, ist der 2. Schritt RISC V Emulation und Programmierung eines (nicht produktiven) Kernels dafür in Rust. Nicht, dass daraus ein Linux Konkurrent werden soll. Nur um die Konzepte zu verstehen (Speicherverwaltung, Sheduler, Rechte, Hardware-I/O, Dateisystem(e),...).
Naja, 5 Tage um c++ von Spaghetti in OO auszubauen, reichen doch noch 3 Wochen um Rust und Kernel zu beherrschen...
Ergänzung ()

Nebenbei muss ich irgendwie noch bissle git verstehen und next Level wäre etwas gui Verständnis (nicht wie man die toll macht, dafür gibbed frontend designer, Nur wie das so grundsätzlich funzt mit den Schnittstellen).
 
Zuletzt bearbeitet:
Schau dir mal die sum()- oder die push_back_vec()-Funktion des fold-Beispiels ganz unten in der main() an:
https://en.cppreference.com/w/cpp/language/fold

Wenn ich es richtig verstanden hab, sollte es das sein, was du suchst.

Anbei ein Beispiel, welches dir vielleicht helfen könnte:
C++:
#include <iostream>
#include <sstream>
#include <optional>

namespace anonymous
{
    enum class Operation
    {
          Minus
        , Modulo
    };

    template<typename ParamT>
    std::optional<ParamT>
    minus( const ParamT a, const ParamT b ) noexcept
    {
        if( true )  // :TODO: Do your tests here
        {
            return a - b;
        }

        return {};
    }

    template<typename ParamT>
    std::optional<ParamT>
    modulo( const ParamT a, const ParamT b ) noexcept
    {
        if( true )  // :TODO: Do your tests here
        {
            return a % b;
        }

        return {};
    }

    template<typename ParamT>
    std::optional<ParamT>
    parseAs( const std::string& string )
    {
        ParamT temporary{};
        if( ( std::istringstream{ string } >> temporary >> std::ws ).eof() )
        {
            return temporary;
        }

        return {};
    }

    template<typename ParamT>
    void testMinus( const std::string& strValueA, const std::string& strValueB )
    {
        const auto valueAopt{ parseAs<ParamT>( strValueA ) };
        const auto valueBopt{ parseAs<ParamT>( strValueB ) };

        if( valueAopt and valueBopt )
        {
            if( auto resultOpt{ minus( *valueAopt, *valueBopt ) } )
            {
                std::cout << *valueAopt << " - " << *valueBopt
                          << " is " << *resultOpt << '\n';

                return;
            }
        }

        // Handle error here
        std::cout << "Invalid input values: "
                  << strValueA << ':' << strValueB << '\n';
    }

    template<typename ParamT>
    void testModulo( const std::string& strValueA, const std::string& strValueB )
    {
        const auto valueAopt{ parseAs<ParamT>( strValueA ) };
        const auto valueBopt{ parseAs<ParamT>( strValueB ) };

        if( valueAopt and valueBopt )
        {
            if( auto resultOpt{ modulo( *valueAopt, *valueBopt ) } )
            {
                std::cout << *valueAopt << " % " << *valueBopt
                          << " is " << *resultOpt << '\n';

                return;
            }
        }

        // Handle error here
        std::cout << "Invalid input values: "
                  << strValueA << ':' << strValueB << '\n';
    }

    std::string userInput()
    {
        std::string strValue;
        std::cin >> strValue;

        return strValue;
    }

} // namespace anonymous


int main()
{
    using namespace anonymous;

    std::cout << "Type 2 numeric values by pressing Enter\n";

    const auto strValueA{ userInput() };
    const auto strValueB{ userInput() };

    for( const auto operation : { Operation::Minus
                                , Operation::Modulo } )
    {
        switch( operation )
        {
        case Operation::Minus:
            testMinus<long double>( strValueA, strValueB );
            break;

        case Operation::Modulo:
            testModulo<std::int64_t>( strValueA, strValueB );
            break;
        }
    }

    return 0;
}
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: scooter010
Vielen dank für das umfangreiche Codebeispiel, das hat dich sicher mehr als 5 Minuten gekostet!
So, nachdem ich herausgefunden habe, dass man VS19 sagen muss, dass es doch bitte einen aktuellen Compiler nutzen soll, habe ich deinen Code compiliert bekommen. Ist schon echt nett, was mit templates möglich ist.

Ich habe jetzt noch ein paar persönliche Probleme bei der Analyse zu bekämpfen.
  1. Ich muss erstmal in die Syntax von Templates einsteigen. Kommt morgen.
  2. Warum müssen in Zeile 42 nicht temporary und std::ws vertauscht werden? Ich will ja den von Whitespace befreiten Stream in temporary zurück geben, oder?
  3. Gebe ich direkt zwei durch ein Leerzeichen getrennte Zahlen ein, dann akzeptiert er diese für die Berechnung. Streng genommen müsste es beim Aufruf von testMinus zu einer exception kommen, da einer der Parameter "strValueB" nicht befüllt worden sein kann, es gab ja nur ein Enter und somit nur einen String, der von cin eingelesen wurde beim Aufruf in Zeile 111. Der Aufruf in Zeile 112 wird dann übersprungen. In der Debug-Konsole sind jedoch strValueA und B befüllt, ich kann mir das nicht erklären. Bitte nicht spoilern, ich komme schon auf die Lösung bis morgen.
Ich glaube, mein Problem bei 3 sind Kompileroptimierungen. Das "Aufräumen" des Strings (parseAs)kann nicht mit einem Haltepunkt versehen werden. Wenn ich in der main() Zeile 111 und Zeile 112 mit Haltepunkten versehe, dann sehe ich, wie die Variablen StrValueA und B direkt korrekt befüllt werden, egal ob mit Leerzeichen oder Zeilenumbruch zwischen den Zahlen. Auch sonstiger "Whitespace" wird direkt entfernt, bevor testminus() überhaupt nur aufgerufen wird. Ich gehe grade davon aus, dass die Compileroptimierung die Funktion "userInput()" und "parseAs()" mit den nacheinanderliegenden Aufrufen in der main() zusammengefasst hat. In diesem Fall jedoch mit Veränderung der Programmlogik. gibt man eine dritte oder noch mehr Zahlen in einer Eingabe ein, werden diese ignoriert.
 
Zuletzt bearbeitet:
Zurück
Oben