Fragen? Antworten! Siehe auch: Alternativlos
Ein Ansatz dafür war C++. In C++ ging es natürlich auch um einen Haufen anderer Dinge, aber es ging eben auch darum, Dinge, die häufig auf die Fresse fallen, wegzuabstrahieren. Man arbeitet nicht mehr mit C-Strings, die mit in-band-Signaling arbeiten (0-Byte = Stringende), sondern man hat eine String-Klasse, und die kennt die Länge. Niemand muss mehr mit strlen über alle Bytes iterieren, um die Länge zu finden.
Aber das ist ja nicht das einzige Problem in strings. Nehmen wir mal an, du hast einen String s, und willst das 100. Zeichen davon haben.
Dann machst du in C oder C++ s[99].
Was aber, wenn der String bloß eine Länge von 10 hat?
Tja, das ist dann ein Bug. Nun könnte man, wenn man eine Klasse in C++ hat, den operator[] überladen, und wenn der Index negativ oder größer als die Stringlänge ist, dann könnte man das erkennen und beispielsweise eine Exception werfen. Dafür sind die da. Für genau sowas.
Aber ... macht C++ das? Nein, machen sie nicht! Der Standard lässt das offen, ob die Implementation das abfangen soll oder nicht. Und dreimal dürft ihr raten, was die Implementationen da draußen folgerichtig alle machen! Richtig! Nicht checken.
Diese Art von Nachlässigkeit ärgert mich auf einem sehr fundamentalen Level. Da krieg ich Pickel und Bluthochdruck von. Wenn du schon den Aufwand auf dich nimmst, eine Abstraktion einzuziehen, dann gibt es echt keine Ausrede, da nicht auch gleich eine Fehlerklasse auszuschließen.
Nun nimmt man in C++ im Allgemeinen Iteratoren und nicht Index. Hat C++ also die Iteratoren richtig gemacht? Nein, auch nicht! Der Iterator ist unabhängig von der Basisklasse. Sagen wir mal, die Basisklasse ist ein Baum, und man hat einen Iterator, der beim 3. Element ist. Und man löscht jetzt dieses Element. Dann ist der Iterator ... ungültig. Warum eigentlich? Der Iterator ist doch eine Abstraktion. Die Abstraktion hätte man so machen können, dass der Container alle offenen Iteratoren in einer Liste hat, und wenn er Iteratoren invalidiert, dann sagt er den Iteratoren Bescheid. Und wenn dann jemand einen von denen verwenden will, dann platzt das Programm. So hätte man das machen sollen. Hat man aber nicht. Stattdessen ist das jetzt halt ein Bug, wenn jemand so einen Iterator verwendet. Und die Regeln dafür, wie man als Programmierer erkennen soll, ob ein Iterator noch gültig sind, sind obskur und weitgehend unbekannt. In typischem Code da draußen gilt das Prinzip Hoffnung.
Ich ärgere mich darüber, weil das unnötig war, und der Grund ist, wieso wir jetzt Rust haben. Rust kann noch mehr als ich hier skizziert habe. Rust verhindert auch Data Races. Ich will Rust nicht kleinreden. Aber man hätte den Bulk von der Zusatzleistung von Rust auch in C++ implementieren können. Nicht mal in der Sprache. In der Library. Mit bestehenden Compilern. Aber wir stehen ja anscheinend auf Schmerzen in der C++-Community.
Warum rante ich hier so lange über diesen alten Punkt rum? Weil es gerade einen Versuch gibt, für C++ ein paar Richtlinien zu erarbeiten. Arbeitet ab jetzt so, dann wird das alles besser. Dieses Projekt wird von einigen der wichtigsten Namen in der C++-Community gemacht. Es liegt offen auf github rum, man kann es dort auch direkt lesen, und es gibt eine Referenzimplementation, auch auf Github, von Microsoft. Ein Großteil von diesem Code arbeitet um Unzulänglichkeiten in irgendwelchen MSVC-Versionen herum, daher ist das so groß, und daher verwaltet Microsoft das auch. Es gibt auch alternative Implementationen.
Einer der Vorschläge aus dieser Library ist so genial wie einfach: span. span ist sowas ähnliches wie std::array -- es schafft einen STL-Container um einen Speicherbereich fester Länge herum. Der Unterschied ist, dass span einen existierenden Speicherbereich nimmt und als Container zugänglich macht, ohne die Ownership davon zu übernehmen. Das kann man also wunderbar z.B. fürs Parsen von einem Byteklumpen verwenden, der gerade über Netzwerk reinkam. Die Abstraktion weiß, wie groß der Speicherbereich ist, und kann dann ein für alle Mal alle Range-Checks unter der Haube erledigen beim Zugriff. Kein Inline-Rumgemurkse mehr mit irgendwelchen halbgaren Checks. Das macht die Abstraktion für dich. Geil!
So geil, dass das in C++20 Einzug erhalten soll. cppreference.com hat es schon dokumentiert. Es gibt zwei Arten von Range Check, die man da so braucht. Erstens: "Gib mir Byte 15" soll explodieren, wenn es nur 14 Bytes sind. Zweitens: "Gib mir mal den String von Byte 15 bis Byte 30" soll explodieren, wenn das nicht vollständig innerhalb des äußeren span liegt. Insbesondere auch dann, wenn Offset und Offset+Länge innerhalb des span liegen, aber es einen Integer Overflow bei der Addition gibt. Und tatsächlich: std::span bietet so eine Operation an. Gib mir den Teil von hier bis da als Unter-Span. Die Methode heißt std::span::subspan. So und was sehen meine entzündeten Augen da?
The behavior is undefined if either Offset or Count is out of range.
ARE YOU FUCKING KIDDING ME?! Wieso überhaupt diesen Scheiß anbieten, wenn der nichts prüft!?!?Meine Güte, C++, so wird das nichts mehr mit euch. Es ist fast so, als WOLLTEN die, dass das immer alles kacke bleibt. Damit man mehr Consulting verkaufen kann oder so? Ich verstehe es nicht. Und dann wundern sich am Ende alle, wieso es so viele Sicherheitsprobleme gibt immer.
Ist nicht alles scheiße hier. Die GSL von Microsoft prüft ordentlich. Die GSL Lite prüft auch (aber nicht ordentlich, ich file gleich mal nen Bug). Aber wenn der Standard es nicht vorschreibt, hilft das ja alles nichts.
Es ist echt zum Heulen.
Gut, auf der anderen Seite werde ich nie arbeitslos mit meiner Security-Firma.
Update: Leser weisen darauf hin, dass man Bounds Checking auch von string und vector kriegen kann. Man muss nur nicht s[10] sondern s.at(10) schreiben. Das stimmt. Es tut nur in der Praxis niemand. Vielleicht weil es keiner weiß. Vielleicht weil Programmierer häufig der Meinung sind, sie brauchen keine Zusatzchecks, weil sie so genial sind. Wer weiß. Ich tippe: Weil es Scheiße aussieht.
Update: Ich freue mich sehr, dass jemand gemerkt hat, dass ich schrieb, das 100. Zeichen im String sei bei s[100]. Ist es nicht. Es ist hinter s[99]. In C und C++ fängt die Zählung bei 0 an. Vielleicht ist doch noch nicht alles verloren. :-)
Update: Ein Leser merkt an, dass Microsoft das Iteratoren-Problem erkannt hat und eine Lösung anbietet. Mal wieder. Microsoft waren auch die ersten, die den inhärenten Integer Overflow in operator new gefixt haben (nachdem ich sie darauf hinwies). Als ich bei clang den entsprechenden Bug gefiled habe, haben die das innerhalb von einem Tag gefixt. g++ brauchte Jahre, bis sie sich durchringen konnten. Wenn wir unsere digitale Infrastruktur so verkommen lassen, dann wird die Zukunft aber trübe aussehen für Open Source!
Update: Eine erschreckende Anzahl von Lesern schreibt mir jetzt, dass Range Checks ja Laufzeit kosten und C++ hat halt versprochen, da neutral zu bleiben. Ich habe vor über zehn Jahren einen Vortrag über Compiler-Optimizer gehalten. Was damals Stand der Technik war, nicht Science Fiction. Und da gibt es eine Sektion über Range Checks. Wisst ihr, was da steht? Guckt selbst! Ist auf meiner Homepage verlinkt. Ich warte solange. Fertig? Da steht: Range Checks kosten nichts. Warum? Zwei Gründe. Erstens kann der Compiler unnötige Range Checks wegoptimieren. Kann nicht nur, tut es. Die bleiben also überhaupt nur in den unoffensichtlichen Fällen da. Und wenn der Range Check im Code landet, dann spielt das auch keine Rolle, weil die CPU im Wesentlichen die ganze Zeit auf den Speicher wartet. Ein einziger L2-Cache-Zugriff dauert lange genug, um gleich mehrere Range Checks in der Latenz komplett zu verbergen. Und wir reden hier von einem Iterator, den du dereferenzieren willst. D.h. da ist garantiert ein Speicherzugriff. So und jetzt Hausaufgabe: Baut mal einen eigenen Vector mit Range Check und tut damit ein paar Dinge. Und dann guckt, ob der Compiler die Range Checks rausoptimiert. Und wenn nicht, dann macht mal einen Benchmark und versucht die Kosten für die Range Checks zu messen. Und DANN könnt ihr zurückkommen und mir Dinge zu erzählen versuchen.