Ist goto xy wirklich so böse?



  • @ DocShoe

    Mach ´ne Funktion draus.

    Toll, warum bin ich da nicht selber drauf gekommen? Allerdings werde ich das setzen des Labels ans Ende der create Methode setzen. Damit ist denn auch mein eigentliches Ziel erreicht, nämlich den Switch nicht zu lang werden zu lassen.

    Und eine Funktion hat ja auch den Vortiel, dass man alle Verdächtigen zusammen hat und diese bei Änderungen nicht erst suchen muss.

    Im Übrigen finde ich die Meinungen zum Einsatz von goto interessant.



  • einwegflasche schrieb:

    @ DocShoe

    Mach ´ne Funktion draus.

    Toll, warum bin ich da nicht selber drauf gekommen?

    Wahrscheinlich weil du meinen Beitrag (der immerhin der erste in diesem Thread war: https://www.c-plusplus.net/forum/p2544013#2544013 EDIT: der zweite natürlich, wenn man den Kopfbeitrag mitzählt 😉 /EDIT) nicht gelesen hast?

    Wozu schreib ich eigentlich noch Antworten 😕 🤡



  • SeppJ schrieb:

    hustbaer schrieb:

    Wobei ich finde dass hier noch eines meiner Lieblingspatterns erwähnt werden sollte, nämlich das Method-Object Pattern. Dadurch dass man damit lokale Variablen zu Membervariablen machen kann, lässt sich "return statt break" zum Abbrechen von verschachtelten Schleifen in sehr vielen Fällen anwenden, wo es mit "normalen" Methoden/Funktionen ... umständlich werden würde.

    Könntest du das erläutern? Ich suche stets nach einer eleganten Lösung für folgendes Problem: Tief verschachtelte, hochkomplexe Rechnung (die man auch am liebsten in Unterfunktionen aufteilen würde), bei der aber unter gewissen Fällen vorzeitig klar wird, was das Ergebnis sein wird. Wie bekommt man dann schnell und sauber dieses Ergebnis heraus, ohne im Rest der Rechnung Laufzeitkompromisse einzugehen?

    Naja... ich meinte dass man damit "multi level break" innerhalb einer Funktion in ein return verwandeln kann. Nicht über Funktionsgrenzen hinweg.
    Ala

    int Fun(int param)
    {
        Foo localFoo = some init expression;
        Bar localBar = some init expression;
        int accumulator = 0;
        DoSomethingThatNeedsUndoing();
        for (...)
        {
            for (...)
            {
                /* use localFoo, localBar, update accumulator */
                if (some condition)
                    goto done;
            }
        }
    done:
        UndoTheThing();
        return accumulator;
    }
    
    // =>
    
    class FunMethod
    {
    public:
        static int Run(int param)
        {
            FunMethod mo(param);
            return mo.RunImpl();
        }
    
    private:
        FunMethod(int param) :
            m_localFoo(some init expression),
            m_localBar(some init expression)
        {
            DoSomethingThatNeedsUndoing();
        }
    
        ~FunMethod()
        {
            UndoTheThing();
        }
    
        int RunImpl()
        {
            int accumulator = 0;
            for (...)
            {
                for (...)
                {
                    /* use m_localFoo, m_localBar, update accumulator */
                    if (some condition)
                        return accumulator;
                }
            }
        }
    
        Foo m_localFoo;
        Bar m_localBar;
    };
    
    int Fun(int param)
    {
        return FunMethod::Run(param);
    }
    

    Was Performance angeht sollte das soweit relativ neutral sein, vorausgesetzt der Compiler trifft die richtigen Inlining-Entscheidungen. Hilft aber natürlich nicht wenn man die beiden for-Loops aufteilen möchte. Also funktionsübergreifendes multi level break ala Exceptions oder longjmp geht damit auch nicht.

    Natürlich kann man es irgendwie reinbasteln, z.B. könnte man auch accumulator zu nem Member machen, und dann nen weiteren Member bool m_done dazu machen. Und diesen dann wo auch immer es Sinn macht checken. Performance davon sollte OK sein, korrekt vorausgesagte Sprünge sind auf den meisten CPUs quasi gratis, und bis auf den ersten und letzten Durchlauf sollten alle "if done" korrekt predicted werden.

    BTW: Natürlich müssten localFoo und localBar nicht unbedingt in Member verwandelt werden. Kann aber oft praktisch sein, wenn man z.B. innerhalb von RunImpl diverse Hilfsfunktionen aufrufen will, die Zugriff auf localFoo und/oder localBar benötigen. Daher hab' ich mal diese Variante skizziert, und nicht die mit nur lokalen Variablen, wo im Prinzip nur das make/break Paar im ctor/dtor übrigbleibt. Was man nämlich vermutlich meistens genau so gut über ne kleine RAII Hilfsklasse für genau dieses make/break Paar lösen könnte, die von Fun() komplett unabhängig sein kann.


  • Mod

    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.


  • Mod

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


Anmelden zum Antworten