Warum sollte man keine versteckten Anweisungen in logischen Ausdrücken ausführen lassen?



  • Hallo, ich lese zur Zeit ein Buch von Dirk Lous über C++. Das Problem ist nicht spezifisch für C++, deswegen poste ich es hier.

    Ich bin jetzt gerade bei Funktionen angekommen und im Codebeispiel gibt es einen Funktionsaufruf im Bedingungsteil einer If Anweisung.

    Als Anmerkung (Für Zeile 23 && zahl_1_10() >= 5) wird gesagt, dass man keine versteckten Anweisungen in logischen Ausdrücken ausführen lassen soll, da es sein kann, dass der betreffende Teilausdruck gar nicht mehr ausgeführt wird.

    Ich verstehe nicht warum das so schlimm ist. Man will ja nicht dass der Teilausdruck ausgeführt wird. wenn hier z.B n kleiner als 20 ist, will ich nicht dass der nächste teil ausgeführt wird.

    #include <iostream>
    #include <ctime>
    #include <cstdlib>
    using namespace std;
    
    int zahl_1_10()
    {
    	return (rand()%10) + 1;
    }
    
    int main()
    {
    
    	time_t t;
    	srand(time(&t));
    
    	int n = 0;
    
    	for (int i = 0; i < 20; i++)
    	{
    		n = i * zahl_1_10();
    
    		if (n > 20 && zahl_1_10() >= 5)
    			cout << " gezogene Zahl: " << zahl_1_10() << endl;
    	}
    	return 0;
    }
    


  • Ich denke es geht Dirk um "Anweisungen" die Nebeneffekte ("side effects") haben.
    Die Funktion zahl_1_10 hat so einen Nebeneffekt, nämlich dass der interne Zustand des Zufallszahlengenerators verändert wird.
    Dieser Nebeneffekt ist allerdings meist egal.

    Und dann ist auf der Aufruf innerhalb einer "if Bedingung" egal.

    Wenn der Nebeneffekt allerdings nicht egal ist, dann sind solche Konstrukte beliebte Fehlerquellen.


  • Mod

    Die Begründung gefällt mir nicht. Kurzschlussauswertung wird schließlich oftmals gezielt dafür benutzt, dass man bedingt Ausdrücke mit Nebeneffekten auswerten kann. Das geht natürlich nach hinten los, wenn man nicht weiß, was Kurschlussauswertung ist. Das muss aber jeder Programmierer ohnehin wissen (wenn man mit Sprachen arbeitet, die dieses Feature haben).

    Ich sehe die Gefahr eher darin, dass man vielen Ausdrücke mit Nebeneffekten nicht auf den ersten Blick ansieht, dass sie Nebeneffekte haben. Wenn man sie dann innerhalb eines großen Gesamtausdrucks versteckt, dann gehen sie beim Lesen schnell unter.



  • Vielen Dank für eure Antworten. Demzufolge ist es nur fehleranfälliger Code so zu schreiben oder?

    Anderes Beispiel:

    if (n > 100 && n++ > 100){
       //tue etwas
    }
    

    Das Inkrementieren von n ist hier ein Aufruf mit Nebeneffekt, weil sich der Wert von n ändert, richtig? Demnach wäre es besser den Code so zu schreiben:

    if(n > 100 && n+1 > 100){
       n++;
       //tue etwas
    }
    


  • SeppJ schrieb:

    Die Begründung gefällt mir nicht. Kurzschlussauswertung wird schließlich oftmals gezielt dafür benutzt, dass man bedingt Ausdrücke mit Nebeneffekten auswerten kann. Das geht natürlich nach hinten los, wenn man nicht weiß, was Kurschlussauswertung ist. Das muss aber jeder Programmierer ohnehin wissen (wenn man mit Sprachen arbeitet, die dieses Feature haben).

    Welche Begründung?

    SeppJ schrieb:

    Ich sehe die Gefahr eher darin, dass man vielen Ausdrücke mit Nebeneffekten nicht auf den ersten Blick ansieht, dass sie Nebeneffekte haben. Wenn man sie dann innerhalb eines großen Gesamtausdrucks versteckt, dann gehen sie beim Lesen schnell unter.

    Ja, genau.
    Was solche Konstrukte zu einer beliebten Fehlerquelle macht.

    Ich sehe Code.
    Ein if.
    Grosse komplizierte Bedingung.
    Das meiste sind reine Tests ohne Nebeneffekte.
    Ich denke: OK, keine Nebeneffekte, und guck nicht genauer.
    Ich ändere.
    Doch Nebeneffekte gewesen, hab ich bloss leider nicht bemerkt.
    ++Fehler;
    🙂

    Idealerweise kombiniert man solche Konstrukte mit Funktionsnamen wie GetFoo und IsBar die in Wirklichkeit aber State verändern 🤡



  • NineT4 schrieb:

    Vielen Dank für eure Antworten. Demzufolge ist es nur fehleranfälliger Code so zu schreiben oder?

    Anderes Beispiel:

    if (n > 100 && n++ > 100){
       //tue etwas
    }
    

    Das Inkrementieren von n ist hier ein Aufruf mit Nebeneffekt, weil sich der Wert von n ändert, richtig? Demnach wäre es besser den Code so zu schreiben:

    if(n > 100 && n+1 > 100){
       n++;
       //tue etwas
    }
    

    Naja ... wenn n > 100 ist auch n > 100.
    Der 2. Test im 1. Beispiel ist also redundant.
    (Der Wert von n++ ist das was in n VOR dem Inkrementieren steht -- für den Wert danach müsstest du ++n schreiben!)

    Und das 2. Beispiel macht nicht das selbe wie das 1.
    Unter der Annahme dass n ein int o.Ä. ist macht er zwar fast immer das selbe, aber eben nicht immer (n+1 könnte überlaufen).

    Leicht modifiziertes Beispiel:

    if (a > 100 && b++ > 100){
       //tue etwas
    }
    
    // ==>
    
    if (a > 100){
        if (b++ > 100){
           //tue etwas
        }
    }
    
    // ==>
    
    if (a > 100){
        auto const old_b = b;
        b++;
        if (old_b > 100){
           //tue etwas
        }
    }
    
    // bzw. wenn nicht mit Überlauf zu rechnen ist auch einfach
    
    if (a > 100){
        b++;
        if (b > 101){
           //tue etwas
        }
    }
    

    Die "auto" Variante finde ich ehrlich gesagt selbst grässlich. Anhand eines so abstrakten Beispiels kann man aber nicht viel dazu sagen. Ich müsste 'was mit dem Wert und den Tests die da gemacht werden verbinden um sagen zu können welche Variante ich im "Realeinsatz" vorziehen würde.


  • Mod

    hustbaer schrieb:

    SeppJ schrieb:

    Die Begründung gefällt mir nicht. Kurzschlussauswertung wird schließlich oftmals gezielt dafür benutzt, dass man bedingt Ausdrücke mit Nebeneffekten auswerten kann. Das geht natürlich nach hinten los, wenn man nicht weiß, was Kurschlussauswertung ist. Das muss aber jeder Programmierer ohnehin wissen (wenn man mit Sprachen arbeitet, die dieses Feature haben).

    Welche Begründung?

    NineT4 schrieb:

    dass man keine versteckten Anweisungen in logischen Ausdrücken ausführen lassen soll, da es sein kann, dass der betreffende Teilausdruck gar nicht mehr ausgeführt wird.

    Klingt für mich so, als wolle er Überraschungen bei der Kurzschlussauswertung vorbeugen.

    NineT4 schrieb:

    Vielen Dank für eure Antworten. Demzufolge ist es nur fehleranfälliger Code so zu schreiben oder?

    Anderes Beispiel:

    if (n > 100 && n++ > 100){
       //tue etwas
    }
    

    Das Inkrementieren von n ist hier ein Aufruf mit Nebeneffekt, weil sich der Wert von n ändert, richtig? Demnach wäre es besser den Code so zu schreiben:

    if(n > 100 && n+1 > 100){
       n++;
       //tue etwas
    }
    

    Das ist genau, was gemeint ist. Die beiden Codestücke tun unterschiedliche Dinge. Was dir aber nicht klar zu sein scheint, weil du Kurzschlussauswertung noch nicht verstanden hast. Bei den Operatoren || und && (und gewissermaßen auch ? , aber da erwartet man dies sowieso) wird die zweite Seite nicht mehr ausgewertet, wenn das Ergebnis bereits feststeht. Ist also bei && die linke Seite schon false , wird die rechte Seite gar nicht mehr angefasst. Und umgekehrt bei || . Dieses Verhalten ist garantiert und wird auch häufig gezielt benutzt, um Nebeneffekte auf der rechten Seite nur in bestimmten Fällen auszuführen.



  • Sorry Es sollte so aussehen:
    ich habe "n" an Stelle von "m" geschrieben

    if (n > 100 && m++ > 100){
       //tue etwas
    }
    
    if(n > 100 && m+1 > 100){ 
        m++; 
        //tue etwas 
    }
    

    Jetzt habe ich es auch verstanden.

    Die beiden Beispiele sind nur äquivalent wenn der linke Teil der UND-Verknüpfung false ist. der rechte Teil wird dann garnicht berücksichtigt.

    Wenn der linke Teil im 1. Beispiel true ist dann wird die rechte Seite ausgewertet, was bedeutet, dass m inkrementiert wird auch wenn die rechte Seite false ist (Das habe ich nichts gewusst).

    Im 2. Beispiel wird m nicht um 1 erhöht, wenn die Bedingung (true && false) ist, da die Wertveränderung nicht in der Bedingung sondern im Anweisungsblock passiert und dieser wird komplett ignoriert.

    Wenn beides true ist, kann - wie hustbaer gesagt hat - die Variable überlaufen (daran habe ich auch nicht gedacht).

    #include <iostream>
    using namespace std;
    
    int main() {
    	short test = 32767;
    
    	cout << test + 1 << endl; //32768
    	cout << test++ << endl; //32767
    
    	return 0;
    }
    


  • Ich sehe da keinen Überlauf.

    Beachte ++n vs n++



  • #include <iostream>
    #include <limits>
    
    #define printMinMaxT( TYPE ) \
      std::cout << #TYPE << std::endl; \
      std::cout << "min: " << std::numeric_limits<TYPE>::lowest() << std::endl; \
      std::cout << "max: " << std::numeric_limits<TYPE>::max() << std::endl;
    
    int main() {
      printMinMaxT(short)
      return 0;
    }
    
    short
    min: -32768
    max: 32767
    

    🙂

    Nochmal von vorne (edit3 - jetzt aber).
    Es ist nur kein Überlauf da implizit auf int gecastet wird.
    Mit folgenden Zeilen kommt es zum Überlauf:

    short test = 32767;
      std::cout << short(test + 1) << std::endl; // -32768  !!!
      std::cout << short(test++) << std::endl;   //32767
    


  • Beachtet aber nebenbei dass signed überläufe undefiniert sind 😉



  • roflo schrieb:

    Beachtet aber nebenbei dass signed überläufe undefiniert sind 😉

    Ja, ein wunderbar schwachsinniges Relikt...



  • Sind die nicht mittlerweile implementation defined?
    Ich merk mir das nie...



  • Shade Of Mine schrieb:

    Ich sehe da keinen Überlauf.

    Beachte ++n vs n++

    if (n > 100 && n++ > 100)
    

    vs.

    if(n > 100 && n+1 > 100){ 
        n++;
    

    Und die haben nur dann das selbe Verhalten, wenn es bei n+1 keinen Überlauf gibt.


  • Mod

    hustbaer schrieb:

    Sind die nicht mittlerweile implementation defined?
    Ich merk mir das nie...

    Nein, es ist nach wie vor undefined. It's not a bug, it's a feature. So kann man signed Typen aggressiver optimieren als unsigned Typen. In diesem konkreten Beispiel ( if (n>100 && n+1>100) ) kann beispielsweise der Test mit n+1 wegoptimiert werden, wenn n signed ist. Denn man kann davon ausgehen, dass n+1 > n ist. Und wenn es doch zu einem Überlauf kommt, ist es eben undefiniertes Verhalten. Bei einem unsigned int müssen hingegen die Rechnung und der Test tatsächlich ausgeführt werden.

    Das sieht in diesem konkreten Fall natürlich ziemlich schwachsinnig aus, das liegt aber da dran, dass das Beispiel nicht viel her gibt. Es gibt sicherlich bessere Beispiele.



  • Nein, es wurde aus dem Bug ein zweifelhaftes "Feature" gemacht.


  • Mod

    Der echte Tim schrieb:

    Nein, es wurde aus dem Bug ein zweifelhaftes "Feature" gemacht.

    Quelle? Begründung?



  • Du glaubst nicht ernsthaft, dass signed overflow im Standard undefined ist damit Compilerbauer hier "optimieren" können?


  • Mod

    Der echte Tim schrieb:

    Du glaubst nicht ernsthaft, dass signed overflow im Standard undefined ist damit Compilerbauer hier "optimieren" können?

    Doch. Wenn es um 1-Komplement vs. 2-Komplement ginge, hätten sie implementation defined geschrieben. Durch undefined sind ziemlich gute Optimierungen möglich, vor allem was die Lauflänge von Schleifen angeht.



  • Wäre das tatsächlich der Grund, dann wäre es aber nur konsequent wenn unsigned overflow auch undefined wäre.

    Zumal man sich damals über derartige Optimierungen ziemlich sicher keine Gedanken gemacht hat. Denn dann hätte man Dinge wie restrict auch direkt eingeführt. Nein, damals hat man andere Sorgen gemacht, z.B. das (zum Glück mittlerweile entfernte) 31 Zeichen-Limit von Bezeichnern.



  • Was damals der Grund war es ursprünglich so zu machen ist die eine Frage.

    Was der Grund war weswegen es mit C++11 bzw. C++14 nicht geändert wurde ist eine andere Frage.

    Kann der selbe Grund sein, muss aber nicht.


Anmelden zum Antworten