Ist goto xy wirklich so böse?



  • SeppJ schrieb:

    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?

    Wenn die Rechnung komplex und lange dauert und Ergebnisse nur selten rauskommen:

    throw ComplexCalculationResult(result);
    

    😉

    (Nicht ernst gemeint!)



  • SeppJ schrieb:

    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?

    ohne goto mit

    break (bei einer Schleifenebene)
    return (bei mehreren)
    

  • Mod

    wob schrieb:

    Wenn die Rechnung komplex und lange dauert und Ergebnisse nur selten rauskommen:

    throw ComplexCalculationResult(result);
    

    😉

    (Nicht ernst gemeint!)

    Wutz schrieb:

    break (bei einer Schleifenebene)
    return (bei mehreren)
    

    Das sind halt beides die Lösungen, die nicht so optimal sind. Auch wenn wobs Vorschlag nicht ernst gemeint ist, ist das doch genau das, was ich möchte: Beliebig viele Ebenen sauber abwickeln. Aber das ist halt ein Missbrauch der Exceptions und Exceptions sind im throw-Fall auch arg langsam.

    break und return sind ja gerade die Mechanismen, die nicht ausreichen, denn damit komme ich nicht aus tief verschachtelten Rechnungen heraus, außer ich verzichte auf Unterfunktionen und schreibe stattdessen die gesamte Rechnung in einer einzigen Funktion. Was doof ist. Das ist ja gerade das Thema dieses Threads, dass so etwas nicht immer ausreicht. In diesem Fall ist es aber sogar noch schlimmer, weil die Alternative kein goto, sondern ein longjmp ist, was erst recht Spagetticode ist und auch voraussetzt, dass man ohne sauberes Stackaufräumen auskommt.



  • on-topic:
    Mach ´ne Funktion draus. Wenn es mehr als zwei Typen gibt geht auch der ternäre Operator wie ihn Arcoth vorgeschlagen hat nicht mehr. Bzw. wird länger und unleserlich.

    const char* label_name( unsigned int Type )
    {
       if( fdat.type == ft_IIRLP )  return "IIR low-pass"; 
       else                         return "IIR high-pass"; 
    }
    
    Filter * FilterFactory::create( InData_t * inp ) { 
        Filter *    fi = 0; 
        Fdata_t     fdat; // input to the concrete constructor 
    ...                   // copy some data to fdat 
        switch( inp->type ) { 
    ... 
            case  ft_IIRLP: 
            case  ft_IIRHP: 
                create_IIR( inp, &fdat );            // calculate the parameters 
                fdat.inbuf = get_inbuf( fdat.taps ); // request the input buffer 
                fi = new F_iir_df2( &fdat );         // create the filter 
                fi->label( label_name( fdat.type );
                break; 
    ... 
            default: 
                break; 
        } 
        return fi; 
    }
    


  • SeppJ schrieb:

    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?

    Ohne jetzt Trollen zu wollen. Es wird immer von einer tief verschachtelten, komplexen Berechnung erzählt. So eine ist mir bisher noch nie vor die Flinte gekommen. Kannst du da mal ein Beispiel aus der echten Welt geben?


  • Mod

    sowas schrieb:

    Ohne jetzt Trollen zu wollen. Es wird immer von einer tief verschachtelten, komplexen Berechnung erzählt. So eine ist mir bisher noch nie vor die Flinte gekommen. Kannst du da mal ein Beispiel aus der echten Welt geben?

    Ja, aber wieso sollte ich mir dermaßen viel Mühe machen, hier anonymisierten Beispielcode zu erstellen? Da säße ich mindestens 40 Minuten dran, bloß um deine Neugierde zu befriedigen. Kannst du dir nicht vorstellen, dass es auch Leute gibt, die Programme schreiben, die wirklich etwas tun?

    Die Kurzvariante, ohne konkreten Code:
    Laufe über eine Menge. Rufe für jedes Element mehrere Funktionen auf, die jeweils wieder über eine andere Funktion von allen anderen Elementen der Menge abhängen. Zähle alle Zahlen zusammen. Nun kann es aber ganz regulär vorkommen, dass eine der Zahlen ∞ ist, d.h. man kann sich auch den Rest der Rechnung sparen, weil klar ist, dass die Summe aller Zahlen auch ∞ sein wird. Daher will man in dem Fall aus der Unterfunktion der Unterfunktion heraus in die Gesamtrechenfunktion springen und dort mit ∞ aussteigen.



  • @ 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 🙂


Anmelden zum Antworten