Schleifen - Wieso keine Sicherheit?



  • for ( short int i = 0; i <= 32767; i++) 
         std::cout << i << "\n";
    

    Wieso setzt C++ am Ende des Schleifenkörpers nicht einfach ein conditional jump, der bei overflow-flag ausgeführt wird? Es wurde in C++ doch einige Sachen extra "sicherer" gemacht, zB strings nicht mehr anfällig für overflows's ... wieso wurde diese kleine Sache nicht implementiert?



  • Der Standard kennt CPU-Flags nicht. Woher soll der Compiler zudem wissen, dass du in diesem Fall bei einem Overflow einen Abbruch haben willst? Wie würde ich dann ausdrücken, dass ich keinen Abbruch haben will?



  • Michael E. schrieb:

    Der Standard kennt CPU-Flags nicht. Woher soll der Compiler zudem wissen, dass du in diesem Fall bei einem Overflow einen Abbruch haben willst? Wie würde ich dann ausdrücken, dass ich keinen Abbruch haben will?

    Hm, klingt für mich wie die einzig sinnvolle Reaktion darauf. Wenn man absichtlich ein Overflow provoziert ( wieso sollte man ? ), kann man am Ende doch immernoch mit Inline-Assembler darauf reagieren, oder sehe ich das falsch?

    Der Standard kennt CPU-Flags nicht -> Wie sind dann Bedingungen umgesetzt?



  • EDIT: uff, zu langsam. Das folgende "Genau" bezieht sich auf den Beitrag von Michael E. /EDIT

    Genau.

    Die for-Schleift wird zwar sehr sehr oft genau so verwendet, ist aber grundsätzlich ein ganz generisches Konstrukt. Da muss es keine Schleifenvariable geben, keinen grösser(-gleich)/kleiner(-gleich) etc.

    Man müsste dazu also entweder die for-Syntax noch weiter aufbohren, oder sonst irgendwelche gröberen Änderungen an der Sprache machen. Und das steht wohl einfach nicht dafür.



  • Das Problem tritt ja auch nicht erst zur Laufzeit auf, sondern kann schon beim Kompilieren erkannt werden. Ein vernünftiger Compiler mit vernünftigen Flags sagt zu dem Code sowas wie:

    warning: comparison is always true due to limited range of data type

    Warum also sollte er noch extra zusätzlichen Binärcode erzeugen, um solche Programmierfehler zur Laufzeit abzufangen?



  • Was der Compiler machen könnte, wäre warnen, dass die Bedingung der for-Schleife immer true ergibt. Allerdings müsste er das dafür erst mal merken, was nach VS11 Assembly nicht der Fall ist.



  • Ohne diese Vermutung belegen zu können, vermute ich einfach mal, dass die Möglichkeit eines overflows absolut erwünscht ist. Die for-Schleife in C/C++ unterscheidet sich von den Schleifen anderer Computersprachen ja auch darin, dass man den "Zähler" nicht nur stur zählen lassen, sondern so ziemlich alles damit machen kann (z.B. +1,-2,+3,-4,..., oder Fibonacci 0,1,1,2,3,5,8,...). Unter dem Gesichtspunkt gibt es keinen Grund, warum ich beschränkt werden soll, wenn ich +32766,+32767,-32768,... zählen möchte (der umgekehrte Effekt war früher bei Spielern von schlecht programmierten Spielen sehr erwünscht 😉 ).

    Anderer Erklärungsversuch:
    Im Gegensatz zu Strings, welche "Datenspeicher" sind, handelt es sich bei der for-Schleife um eine Kontrollstruktur. Als Programmierer weißt du zur Entwurfszeit nicht, welche Daten in deinem String landen, daher muss der String gegen jeden Blödsinn abgesichert werden, die ein Benutzer damit anstellen kann.
    Die for-Schleife dagegen kontrolliert DEINEN Programmfluss. Du bist der Programmier, ergo entscheidest du was und wie oft das Programm machen soll. Auch wenn die Abbruchbedingung der Schleife aus einer Benutzereingabe stammen sollte, so verfügst lediglich du über das Wissen, warum nur eine Höchstzahl an Schleifendurchläufen möglich sein soll (in deinem Bsp. ist der Zähler nur ein short, kein int. Das kann der Benutzer nicht wissen, muss also vorher abgefangen werden).

    Ich gebe dir allerdings recht, dass zumindest der Compiler für solche offensichtliche Fehler wie den hier

    for(short i=0; i<100000; ++i){}
    

    eine Warnung ausspucken sollte (hat mein g++ 4.6.1 selbst mit -Wall -pedantic nicht gemacht 😞 ). Dass die Abbruchbedingung hier nicht erreicht werden kann ist offensichtlich - trotzdem kein Grund overflows komplett zu verbieten. Das hier funktioniert nämlich:

    for(short i=32000; i<-32000 || i>0; ++i){}
    


  • Ja, eine Warning wäre gut.
    Ich wundere mich auch gerade sehr dass keine kommt, aber nachdem ich es gerade selbst probiert habe (mit MSVC) muss ich es wohl glauben 🙂



  • (gast) schrieb:

    Unter dem Gesichtspunkt gibt es keinen Grund, warum ich beschränkt werden soll, wenn ich +32766,+32767,-32768,... zählen möchte

    Overflow von signed Typen ist UB.



  • cvcv schrieb:

    Der Standard kennt CPU-Flags nicht -> Wie sind dann Bedingungen umgesetzt?

    Du darfst dir das nicht so vorstellen, dass der C++-Standard sagt, in welche Assembler-Instruktionen ein C++-Code übersetzt werden soll, sondern die Sprache läuft quasi auf einer abstrakten Maschine. Und so sagt der Standard dann auch nur, dass je nach Auswertung der Bedingung des ifs unterschiedlicher Code ausgeführt wird. Wie das zu bewerkstelligen ist, wird dabei offen gelassen.



  • Das erzeugt nicht auf allen Maschinen einen Overflow - es gibt auf der Welt nicht nur x86 und x86-64, wo sich short == 16 Bit eingebürgert hat. Unter UNICOS (das findet man auf Crays) ist short üblicherweise ganze 64 Bit breit (d.h. sizeof(short) == sizeof(int) == sizeof(long)), und da passt 32768 locker rein.

    Ich wüsste auch nicht zu sagen, ob alle CPUs Overflow flaggen.


  • Mod

    (gast) schrieb:

    Ich gebe dir allerdings recht, dass zumindest der Compiler für solche offensichtliche Fehler wie den hier

    for(short i=0; i<100000; ++i){}
    

    eine Warnung ausspucken sollte (hat mein g++ 4.6.1 selbst mit -Wall -pedantic nicht gemacht 😞 ).

    Solche Sachen mahnt der GCC normalerweise erst ab Wextra an (Tut er in diesem Fall auch wirklich!). Wall ist eher für Sachen, die wirkliche Laufzeitfehler ergeben. Dieses hier ist bloß eine ungewöhnliche Programmierstruktur mit genau definiertem Verhalten, welches auch keinen Fehler hervorruft.



  • SeppJ schrieb:

    Dieses hier ist bloß eine ungewöhnliche Programmierstruktur mit genau definiertem Verhalten, welches auch keinen Fehler hervorruft.

    Eigentlich handelt es sich hier um Undefined Behaviour. Das müsste in http://blog.regehr.org/archives/161 stehen. Schlag mich, wenns nicht so ist.



  • @SeppJ: Okay, überredet 😉 Irgendwie muss man ja auch absichtliche "Endlos"-Schleifen programmieren können
    Sachen wie

    while(1!=2){
        ...
        if(condition){break;}
    }
    

    sind ja weit verbreitet.



  • (gast) schrieb:

    @SeppJ: Okay, überredet 😉 Irgendwie muss man ja auch absichtliche "Endlos"-Schleifen programmieren können
    Sachen wie

    while(1!=2){
        ...
        if(condition){break;}
    }
    

    sind ja weit verbreitet.

    Äh, while (true) oder einfach for (;;) ?


  • Mod

    Michael E. schrieb:

    SeppJ schrieb:

    Dieses hier ist bloß eine ungewöhnliche Programmierstruktur mit genau definiertem Verhalten, welches auch keinen Fehler hervorruft.

    Eigentlich handelt es sich hier um Undefined Behaviour. Das müsste in http://blog.regehr.org/archives/161 stehen. Schlag mich, wenns nicht so ist.

    Woah! Das ist aber mal eine obskure Klausel! Ich erinnere mich zwar da dran, diese mal gelesen zu haben, aber an diese Konsequenzen hätte ich nie gedacht. Das hat ja richtig weitreichende Folgen für die Standardkonformität harmloser Programme!
    Dann lass uns vielleicht lieber annehmen, dass das Programm gutes altes C89 ist mit definiertem Überlaufverhalten und ohne Klauseln über das Verhalten von Schleifen. 🙂



  • Noch ne kleine Anmerkung: In C++11 ist dieses Verhalten explizit nicht mehr auf Schleifen begrenzt, sondern gilt beispielsweise auch bei endlosen rekursiven Aufrufen. Hab die entsprechende Stelle aber gerade nicht zur Hand.

    Edit: §1.10.24:

    The implementation may assume that any thread will eventually do one of the following:
    —terminate,
    —make a call to a library I/O function,
    —access or modify a volatile object, or
    —perform a synchronization operation or an atomic operation.
    [Note:This is intended to allow compiler transformations such as removal of empty loops, even when
    termination cannot be proven. —end note]



  • @Michael E.
    Ja, die ganze Geschichte mit "Programm/Funktion das nicht terminiert => UB => Compiler dürfen ganz wild Dinge wegoptimieren" hat ja durchaus für einige Diskussionen gesorgt.
    Das Argument des "pro" Lagers war dabei immer dass das bestimmte Optimierungen erlaubt die sonst nicht möglich wären.

    Was mich aber interessieren würde: gibt es da ein gutes Beispiel für eine Optimierung, die ohne diese Regel nicht möglich wäre, und wo auch etwas sinnvolles dabei rauskommt?

    Ich kann mir nämlich vorstellen dass bestimmte Optimierungen dadurch vielleicht einfacher werden, aber mir fällt nix ein wo es ohne diese Regel wirklich nicht möglich wäre.


  • Mod

    Die leere Endlosschleife wird schneller ausgeführt 🙂 .

    Um etwas sinnvoller zu werden: Gegeben ein Programmteil

    unsigned N;
    cin >> N;
    unsigned counter = 0;
    for (unsigned short i = 0; i < N; ++i)
     ++counter;
    // N im Folgenden nicht mehr benutzt.
    

    Könnte komplett durch

    unsigned counter;
    cin >> counter;
    

    ersetzt werden, wenn man weiß, dass die Schleife terminiert.

    Solche Fälle sollten sich leicht mehr (und sinnvoller) konstruieren lassen, wenn das Ergebnis einer Schleife für den Compiler berechenbar von den Kontrollvariablen abhängt (gar nicht so selten!), aber in einigen Ausnahmefällen die Schleife gar nicht abbrechen würde.



  • Wenn die Eingabe fehlschlägt, enthält die Variable einen undefinierten Wert.



  • 314159265358979 schrieb:

    Wenn die Eingabe fehlschlägt, enthält die Variable einen undefinierten Wert.

    War vorher auch so.


Anmelden zum Antworten