Ist goto xy wirklich so böse?


  • Global Moderator

    Schade. Ich hatte auf die geniale Lösung gehofft, an die ich bisher nie gedacht hatte. Zugegeben unwahrscheinlich, da ich recht intensiv nachgedacht und gegoogelt habe. Dann bleibe ich bei dem Flag in der Art des von dir vorgeschlagenen m_done , welches ich jetzt schon benutze. Das ist nämlich in der Tat akzeptabel von der Performance und der Gewinn durch die Abkürzung überwiegt bei weitem diese Kosten.



  • Nicht durchdacht und entsprechend nicht wirklich ernst gemeint: könntest du nicht über boost::context + evtl. einem entsprechenden Context Parameter einen "continuation point" setzen und in deiner Funktion, die du verlassen willst sagen, jetzt will ich zu dem im Context festgelegten Punkt springen?



  • @ hustbaer

    Wahrscheinlich weil du meinen Beitrag ... nicht gelesen hast?

    Gelesen schon, aber mein alternder Denkapparat macht leider nicht immer sofort Klick.



  • @Mechanics
    Das wäre dann longjmp in Verkleidung, oder? (IIRC verwendet boost::context longjmp ...?)
    Wäre interessant wie die Performance davon ist.



  • So in der Art...



  • Ich habe im Clang sourcecode auch ein goto gesehen, es wurde verwendet, um rekursive Funktionsaufrufe zu verhindern (goto wird an mehreren Stellen verwendet, aber wofür weiß ich nicht überall, wohl teilweise auch um code zu überspringen).

    /// LexTokenInternal - This implements a simple C family lexer.  It is an
    /// extremely performance critical piece of code.  This assumes that the buffer
    /// has a null character at the end of the file.  This returns a preprocessing
    /// token, not a normal token, as such, it is an internal interface.  It assumes
    /// that the Flags of result have been cleared before calling this.
    bool Lexer::LexTokenInternal(Token &Result, bool TokAtPhysicalStartOfLine) {
    LexNextToken:
    // ...
        // We know the lexer hasn't changed, so just try again with this lexer.
        // (We manually eliminate the tail call to avoid recursion.)
        goto LexNextToken;
    // ...
    }
    


  • @HarteWare
    Ich glaube dir dass du das dort so gesehen hast. Und vermutlich kann man argumentieren dass es mit dem goto irgendwie klarer wird oder schöner ist*. Ich würde das aber dennoch mit nem ganz normalen Loop ersetzen:

    /// ...
    bool Lexer::LexTokenInternal(Token &Result, bool TokAtPhysicalStartOfLine) {
        while (true) {
        // ...
            // We know the lexer hasn't changed, so just try again with this lexer.
            // (We manually eliminate the tail call to avoid recursion.)
            continue; // Look ma, no goto!
        // ...
        }
    }
    

    Wenn das langsamer sein sollte als die goto-Version, dann läuft irgendwas extrem falsch.

    *: Vielleicht ist das für Leute die gewohnt sind rekursive Parser zu schreiben pfui-spinne-hässlich, weil es wie ein Loop aussieht. Naja aber, Pech, Tail-Recursion ist nunmal das selbe wie ein Loop. 🤡



  • Mich würde vor allem eine saubere Möglichkeit für (geschachtelte) Lambdas interesieren...

    #include <iostream>
    
    template <typename Callback>
    void my_for(size_t start, size_t end, Callback callback)
    {
    	for (size_t i = start; i < end; ++i) { callback(i); }
    }
    
    int main()
    {
    	my_for(0, 3, [](size_t i)
    	{
    		my_for(0, 3, [&i](size_t j)
    		{
    			if (i == 0 && j == 1) 
    				return; // Überspringt nur einen Durchgang
    			std::cout << i << " " << j << "\n";
    		});
    	});
    }
    

    Momentan mache ich das mit throw obwohl das ja verpönt ist (und angeblich ja auch einen ziemlich hohen Overhead hat)... Sehe aber keine "schöne" Alternative?

    Weil hier geht ja nichtmal was mit goto (nicht dass ich das dann schön finden würde).

    Ich fände es eigentlich nicht schlecht wenn man throw ohne großen Overhead ganz "normal" verwenden könnte, nicht nur für Exceptions... Schließlich kann man auch nicht-Exception Objekte werfen.



  • In diesem Fall easy, da die Lambdas keinen Returnwert haben:

    template <typename Callback>
    bool my_for(size_t start, size_t end, Callback callback)
    {
        for (size_t i = start; i < end; ++i)
            if (!callback(i))
                return false;
        return true;
    }
    

    Lässt sich aber auch allgemein anwenden. In dem Fall dann halt z.B. über nen bool& stop Parameter oder indem man Tuples zurückgibt.

    Oder auch, ganz anderer Ansatz: nich auf alles mit Lambdas draufhauen wo man mit irgendwas draufhauen kann.



  • hustbaer schrieb:

    Wäre interessant wie die Performance davon ist.

    Ja, wär schon interessant, wenn mal jemand sowas ausprobiert ^^
    Und nein, ich glaub nicht, dass die intern longjmp verwenden, das ist denk ich etwas komplizierter.



  • hustbaer schrieb:

    In diesem Fall easy, da die Lambdas keinen Returnwert haben:

    Nee, das is nix, dann muss ich ja immer auch einen Wert in der Schleife zurückgeben auch wenn ich gar nicht breaken will.

    hustbaer schrieb:

    Lässt sich aber auch allgemein anwenden. In dem Fall dann halt z.B. über nen bool& stop Parameter oder indem man Tuples zurückgibt.

    Du meinst z.B. so?

    #include <iostream>
    
    template <typename Callback>
    void my_for(size_t start, size_t end, Callback callback)
    {
    	bool stop = false;
    	for (size_t i = start; i < end; ++i) { callback(i, stop); if (stop) return; }
    }
    
    int main()
    {
    
    	my_for(0, 3, [](size_t i, bool &early_stop_1)
    	{
    		my_for(0, 3, [&i, &early_stop_1](size_t j, bool &early_stop_2)
    		{
    			if (i == 0 && j == 1)
    			{
    				early_stop_1 = true;
    				early_stop_2 = true;
    			}
    			std::cout << i << " " << j << "\n";
    		});
    	});
    }
    

    Klar geht, ist dann halt mehr oder weniger das äquivalent zu Zustands-Variablen wenn man aus einer "normalen" verschachtelten Schleife breaken will. Schön find ichs halt nicht (wie bei "normalen" Schleifen eben auch).

    hustbaer schrieb:

    Oder auch, ganz anderer Ansatz: nich auf alles mit Lambdas draufhauen wo man mit irgendwas draufhauen kann.

    Dafür sind die doch da, gerade bei so kleinem body? Also ich mag Lambdas 🙂



  • happystudent schrieb:

    hustbaer schrieb:

    In diesem Fall easy, da die Lambdas keinen Returnwert haben:

    Nee, das is nix, dann muss ich ja immer auch einen Wert in der Schleife zurückgeben auch wenn ich gar nicht breaken will.

    Nö, musst du nicht.
    Du musst nur für zwei recht unterschiedliche Dinge, nämlich für die Variante die vorzeitigen Abbruch erlaubt und die die keinen vorzeitigen Abbruch erlaubt, zwei unterschiedliche Funktionen machen.

    happystudent schrieb:

    hustbaer schrieb:

    Lässt sich aber auch allgemein anwenden. In dem Fall dann halt z.B. über nen bool& stop Parameter oder indem man Tuples zurückgibt.

    Du meinst z.B. so?
    (snip)

    Klar geht, ist dann halt mehr oder weniger das äquivalent zu Zustands-Variablen wenn man aus einer "normalen" verschachtelten Schleife breaken will. Schön find ichs halt nicht (wie bei "normalen" Schleifen eben auch).

    Ich meins in weniger hässlich und mit nur einer "stop" Variable für beide Schleifen.

    happystudent schrieb:

    hustbaer schrieb:

    Oder auch, ganz anderer Ansatz: nich auf alles mit Lambdas draufhauen wo man mit irgendwas draufhauen kann.

    Dafür sind die doch da, gerade bei so kleinem body? Also ich mag Lambdas 🙂

    Ja, klar. Das schöne neue Werkzeug einfach mal für jede Aufgabe einsetzen, obwohl sich die Aufgabe mit Händen und Füssen dagegen wehrt, ist immer volle schlau.

    ps:

    happystudent schrieb:

    my_for(0, 3, [](size_t i)
        {
            my_for(0, 3, [&i](size_t j)
            {
    

    &i
    *facepalm*



  • hustbaer schrieb:

    Ja, klar. Das schöne neue Werkzeug einfach mal für jede Aufgabe einsetzen, obwohl sich die Aufgabe mit Händen und Füssen dagegen wehrt, ist immer volle schlau.

    Weiß ja nicht wo du rausliest dass ich das für jede Aufgabe einsetze. 😕

    Nur so: Das war ein Minimalbeispiel. Ich ersetze nicht jede for-Schleife durch ein Lambda, falls du das dachtest.

    Gerade beim Erstellen einer eigenen for Schleife wehrt sich die Aufgabe aber nicht "mit Händen und Füßen", sondern ist geradezu prädestiniert dafür, z.B. concurrency::parallel_for oder std::for_each .

    hustbaer schrieb:

    &i
    *facepalm*

    Äh, was ist daran auszusetzen? Wenn ich in der inneren Schleife auf i zugreifen können will muss ich es auch capturen.

    Und da ich i , wie in einer normalen inneren for-Schleife ändern können will, capture by reference.

    Wenn du hier schon facepalmst dann bitte mit Begründung, bin gespannt 🙂



  • happystudent schrieb:

    Gerade beim Erstellen einer eigenen for Schleife wehrt sich die Aufgabe aber nicht "mit Händen und Füßen", sondern ist geradezu prädestiniert dafür, z.B. concurrency::parallel_for oder std::for_each .

    Naja sie wehrt sich zumindest ein bisschen, wenn du die Schleife vorzeitig abbrechen willst. Und std::for_each ist IMO auch ein ganz schlechtes Beispiel, weil in C++11 mMn. völlig redundant. parallel_for macht natürlich Sinn.

    happystudent schrieb:

    Und da ich i , wie in einer normalen inneren for-Schleife ändern können will, capture by reference.

    Wenn du hier schon facepalmst dann bitte mit Begründung, bin gespannt 🙂

    Die Möglichkeit i ändern zu können macht in deinem Beispiel überhaupt keinen Sinn. Schlimmer noch: wenn man so etwas wie dein "my_for" bastelt, dann will man normalerweise gar nicht dass der Funktor die Laufvariable ändern kann.

    Man *kann* das machen, wenn es denn wirklich Sinn macht, und natürlich *kann* es Sinn machen. Dadurch werden aber Dinge die eigentlich Implementierungdetails der Funktion sind zum Contract erhoben - auch wenn man sich darüber vielleicht nicht im klaren ist und es nicht dokumentiert.

    Nämlich z.B. wie die Laufbedingung ist. Also ob i != end oder i < end . Oder ob es evtl. sogar i >= begin && i < end ist - was nötig wäre wenn man dem Funktor auch erlauben möchte abzubrechen indem man i < begin setzt.
    Weiters geht ein recht angenehmer Nebeneffekt der Funktion verloren, nämlich dass man die Laufvariable eben gerade nicht ändern kann. Durch diesen komplizierteren Contract schränkst du nicht nur die Implementierung ein (schlechtere Performance wegen beidseitigem Range-Check) sondern machst das ganze auch aufwendiger zu verstehen. Also aufwendiger richtig anzuwenden, und ganz wichtig: auch aufwendiger zu lesen, da der Leser sich eben nicht mehr darauf verlassen kann dass die Schleife einfach immer genau 1x von vorn bis hinten durchpfeifft ohne was zu überspringen oder zu wiederholen.

    Und da aus dem Beispiel kein Grund erkennbar ist wieso man i ändern wollen würde (und das Beispiel i auch gar nicht ändert), ist dieses Detail in diesem Beispiel ein *facepalm*. Und nochmal mehr, weil es nichtmal gehen würde - der äussere Funktor nimmt i ja by-value.

    Alles in allem bleibt also der Eindruck dass hier entweder
    - grundsätzlich gerne by-reference gecaptured wird, was definitiv einen dicken *double-facepalm* verdient oder
    - ein Werkzeug wegen "könnte ja mal wer machen wollen" unnötig flexibel und damit auch unnötig komplex gemacht wurde - was zumindest einen einfachen *facepalm* verdient.

    happystudent schrieb:

    hustbaer schrieb:

    Ja, klar. Das schöne neue Werkzeug einfach mal für jede Aufgabe einsetzen, obwohl sich die Aufgabe mit Händen und Füssen dagegen wehrt, ist immer volle schlau.

    Weiß ja nicht wo du rausliest dass ich das für jede Aufgabe einsetze. 😕

    Ja, damit hast du Recht. Sorry!



  • hustbaer schrieb:

    Und std::for_each ist IMO auch ein ganz schlechtes Beispiel, weil in C++11 mMn. völlig redundant.

    Gab es eigentlich std::for_each und range based for schon immer zusammen? Weil wenn ja, macht das wirklich irgendwie wenig Sinn 😃

    hustbaer schrieb:

    Die Möglichkeit i ändern zu können macht in deinem Beispiel überhaupt keinen Sinn.

    Ok, jetzt verstehe ich was du meinst. Das stimmt natürlich, wollte für das Beispiel halt möglichst nahe an eine "richtige" for-Schleife rankommen - da kann man ja ohne weiteres auch i ändern. In der Realität würde ich das aber natürlich nicht capturen, wenn nicht notwendig.

    hustbaer schrieb:

    Ja, damit hast du Recht. Sorry!

    Kein Problem, aber es stimmt natürlich auch dass man das sparsam einsetzen sollte... Bei zwei Schleifen gehts noch, aber mehr würde ich dann wahrscheinlich auch nur ungern verwenden.



  • happystudent schrieb:

    hustbaer schrieb:

    Und std::for_each ist IMO auch ein ganz schlechtes Beispiel, weil in C++11 mMn. völlig redundant.

    Gab es eigentlich std::for_each und range based for schon immer zusammen? Weil wenn ja, macht das wirklich irgendwie wenig Sinn 😃

    Nein, std::for_each ist C++98 und range based for ist C++11. Aber range based for und Lambdas sind beide C++11. Es gibt also keinen Grund (zumindest keinen der mir einfällt) std::for_each mit Lambdas zu verwenden. Also ausser partiellem C++11 Support oder sowas.

    happystudent schrieb:

    hustbaer schrieb:

    Die Möglichkeit i ändern zu können macht in deinem Beispiel überhaupt keinen Sinn.

    Ok, jetzt verstehe ich was du meinst. Das stimmt natürlich, wollte für das Beispiel halt möglichst nahe an eine "richtige" for-Schleife rankommen - da kann man ja ohne weiteres auch i ändern.

    Ja, sowas, also dass man versucht etwas anderes möglichst 1:1 nachzubilden, kann einem schnell mal passieren. Ist aber oft gar nicht so gut, weil man damit eben oft etwas schafft was unnötig flexibel und damit unnötig kompliziert ist. In einem Beispiel natürlich nicht tragisch, aber wenn man so production Code entwirft finde ich das gar nicht so gut.



  • hustbaer schrieb:

    Es gibt also keinen Grund (zumindest keinen der mir einfällt) std::for_each mit Lambdas zu verwenden.

    Iteratoren-Paar statt Range? Wir haben schließlich immer noch kein iterator_pair in der Standardbibliothek...



  • OK, das ist ein Grund der mir nicht eingefallen ist 🙂
    Wobei, wenn man es öfter braucht, ... die paar Zeilen die man für ne minimale iterator_pair Implementierung braucht... das könnte sich schon auszahlen.
    Und ich glaube ich würde das lieber machen als nur deswegen std::for_each zu verwenden. Bzw. ich würde boost::make_iterator_range() verwenden, aber angenommen ich hätte keine Boost...