asserts müssen unbedingt korrigiert werden
(kristoff.it)- assert ist ein Mittel, um Vorbedingungen, Nachbedingungen und Invarianten im Code festzuhalten; Einschränkungen, die sich über das Typsystem erzwingen lassen, sollten vorzugsweise mit Sprachfunktionen ausgedrückt werden
- Zigs
std.debug.assertist kein Makro, sondern eine normale Funktion, die mitunreachableunerreichbare Pfade markiert und auch für Optimierungen genutzt wird - In Debug und ReleaseSafe führt ein fehlgeschlagenes assert per panic zum Absturz, in ReleaseFast und ReleaseSmall kann es hingegen als unchecked illegal behavior zu Fehlverhalten kommen
- Wer asserts in Produktion abschaltet, verliert die Chance, falsche Annahmen früh zu entdecken; später kann Code dann von falschen asserts abhängen und in Schwachstellen münden
- Ob man ReleaseSafe oder ReleaseFast wählt, hängt von den Prioritäten des Programms ab; entscheidend ist aber, asserts nicht pauschal abzuschalten, sondern falsche asserts zu korrigieren
Rolle von assert und Zigs Standardverhalten
- assert ist ein Mittel, um im Code auszudrücken, dass Bedingungen wie „dieses Argument kann nicht null sein“ oder „diese Ganzzahl kann nicht gerade sein“ immer wahr sein müssen
- Beispiel:
assert(my_arg != null);,assert(my_num % 2 != 0); - Wenn sich eine Einschränkung über das Typsystem erzwingen lässt, ist eine Sprachfunktion einem assert vorzuziehen
- In Zig kann ein gewöhnlicher Pointer
*Foonicht null sein, während ein optionaler Pointer?*Foonull sein darf, aber vor dem Zugriff eine Prüfung erzwingt
- Beispiel:
- assert eignet sich zur Beschreibung von Vorbedingungen, Nachbedingungen und Invarianten
- Gute asserts können Programmierfehler wirkungsvoller aufdecken als Unit-Tests
- In Kombination mit Fuzzing kann die Wirkung von asserts noch größer sein
Zigs unreachable und assert
- Zigs assert basiert auf
unreachable, einer Sprachfunktion zur Markierung falscher Codepfade- In einem
switchlassen sich unerreichbare Zweige etwa als.a => unreachablemarkieren unreachablekann sowohl als Statement als auch dort verwendet werden, wo ein Ausdruck beliebigen Typs erwartet wird- Es ist nicht nötig, für unerreichbare Fälle künstlich einen Platzhalterwert zu erzeugen
- In einem
std.debug.assertin der Zig-Standardbibliothek ist so implementiertpub fn assert(ok: bool) void { if (!ok) unreachable; // assertion failure }- Die Information aus
unreachablekann für Optimierungen genutzt werden- Der Compiler kann unerreichbare Pfade entfernen, diese Information weiterreichen und dadurch nichtlokale Optimierungen durchführen
- Nicht jedes assert führt zu mehr Leistung, aber es sind auch Optimierungen möglich, die Programmierer nicht leicht vorhersehen
Build-Modi und Laufzeitsicherheit
- Zig kennt die Build-Modi Debug, ReleaseSafe, ReleaseFast und ReleaseSmall
- Diese Einstellung muss nicht zwingend global für das gesamte Programm gelten
- Abhängigkeiten können jeweils in unterschiedlichen Modi gebaut werden, und mit
@setRuntimeSafetylässt sich die Laufzeitsicherheit sogar blockweise innerhalb einer Funktion anpassen
- Ein fehlgeschlagenes assert gilt in Zig als „illegal behavior“
- In den checked Modi Debug, ReleaseSafe und
@setRuntimeSafety(true)stürzt das Programm mit panic ab - In den unchecked Modi ReleaseFast, ReleaseSmall und
@setRuntimeSafety(false)tritt „unchecked illegal behavior“ auf, wodurch das Programm falsch arbeiten kann
- In den checked Modi Debug, ReleaseSafe und
- Das Ergebnis von unchecked illegal behavior ist nicht garantiert
- Im gezeigten
switchkann es wegen der aktuell erzeugten Maschinencodes so aussehen, als würde in einen anderen Zweig gesprungen - Mit einer anderen Compilerversion kann das Fehlverhalten völlig anders aussehen
- Das zugehörige Verhalten ist im godbolt-Beispiel zu sehen
- Im gezeigten
- Wie sich assert und ein nachfolgendes
switchin ReleaseSafe und ReleaseFast unterscheiden, zeigt ein weiteres godbolt-Beispiel- In ReleaseFast kann die Funktion am Ende alle Vergleiche überspringen und einfach
truezurückgeben - Auf genau diese Art von Optimierung sind Videospiele und andere Echtzeit-Medienanwendungen stark angewiesen
- In ReleaseFast kann die Funktion am Ende alle Vergleiche überspringen und einfach
Zig-assert ist kein Makro
- Zigs
std.debug.assertist kein Makro, sondern eine normale Funktion- Zig hat keine Makros
- Gerade für C/C++-Entwickler ist das beim Einstieg in Zig oft überraschend
- In C/C++ ist es üblich, dass beim Deaktivieren von asserts der gesamte assert-Aufruf mitsamt dem übergebenen Ausdruck so behandelt wird, als wäre er auskommentiert
- Deshalb sollten in C/C++ keine Ausdrücke mit Seiteneffekten in asserts stehen
- Wird assert deaktiviert, kann die betreffende Operation selbst verschwinden
- In Zig werden nach den Regeln für Funktionsaufrufe die Argumente vor dem Funktionsaufruf ausgewertet
- Unabhängig von der internen Logik von
std.debug.assertwird der Ausdruck des Arguments ausgewertet - Deshalb kann man auch Ausdrücke mit Seiteneffekten in assert verwenden, etwa so
// assert that the remove operation is not a noop: assert(my_map.remove("expected-to-exist")); - Unabhängig von der internen Logik von
- Umgekehrt gilt: Wenn die Berechnung der assert-Bedingung komplexe Operationen erfordert, werden diese im unchecked Modus nicht zwingend entfernt
- In solchen Fällen sollte der Code mit
comptime ifgeschützt werden
const builtin = @import("builtin"); if (builtin.mode == .Debug) { var condition = ...; // whatever bookkeeping is necessary // to compute the condition assert(condition == .ok); } - In solchen Fällen sollte der Code mit
- Wer an die Semantik aus C/C++ gewöhnt ist, findet das möglicherweise ungewohnt, aber in Zig ist implizit vorausgesetzt, dass asserts normalerweise nicht deaktiviert werden
Das Problem mit deaktivierten asserts in Produktion
- Bei asserts gibt es im Wesentlichen drei Möglichkeiten
- Man belässt sie als Laufzeitprüfung und lässt den Prozess bei einem Fehlschlag per panic abstürzen
- Man nutzt asserts für Performance-Optimierungen und nimmt Fehlverhalten in Kauf, wenn ein assert falsch ist
- Man deaktiviert asserts vollständig
std.debug.assertunterstützt die vollständige Deaktivierung von asserts standardmäßig nicht- Mit einem eigenen assert, das intern ein Build-Flag prüft, lässt sich ein Verhalten näher an C/C++ nachbauen
- Der Wunsch, asserts abzuschalten, entsteht meist aus einer Kombination zweier Gründe
- Man möchte die Laufzeitprüfung wegen Kosten oder Abstürzen der Anwendung nicht beibehalten
- Man vertraut nicht darauf, dass asserts immer korrekt sind, und fürchtet Fehlverhalten, wenn sie für Optimierungen verwendet werden
- Wie matklad in der zugehörigen Diskussion in Erinnerung gerufen hat, gibt es Situationen mit legitimen technischen Gründen, Abstürze zu vermeiden
- Für allgemeine Software Absturzvermeidung zum Standard zu machen, wird jedoch als schlechte Wahl bewertet
- Wer asserts deaktiviert, sorgt dafür, dass Bedingungen, die angeblich unmöglich sind, beim tatsächlichen Auftreten das Programm nicht stoppen
- Das Programm läuft unter falschen Annahmen weiter, was bereits eine Form von Fehlverhalten ist, auch wenn es sich nicht um unchecked illegal behavior handelt
- Unchecked illegal behavior oder undefiniertes Verhalten in C sind deshalb gefährlich, weil sie das Programm in eine weird machine verwandeln können
- In ausreichend komplexer Software kann es auch ohne UIB zu unbeabsichtigten, verdrehten Ausführungspfaden kommen
- Wenn ein assert zur Laufzeit falsch wird, bedeutet das einen Verstoß gegen die Spezifikation und kann allein dadurch unbeabsichtigte Aktionen auslösen
- SQL-Injection ist ein konkretes, weitverbreitetes Beispiel für weird-machine-artiges Fehlverhalten ganz ohne UIB
- Wenn die Kosten von Fehlverhalten zu hoch sind, sollte man asserts eingeschaltet lassen
- Wenn Performance extrem wichtig ist und das Risiko von Fehlverhalten tragbar erscheint, ist es sinnvoller, asserts als Optimierungschance zu nutzen
- Wer asserts deaktiviert, verzichtet auf Performance und wiegt sich zugleich leicht in falscher Sicherheit
Wie falsche asserts eine Codebasis täuschen
- Das zentrale Risiko besteht darin, dass falsche asserts in Tests unbemerkt bleiben und erst in Produktion fehlschlagen können
- Wenn garantiert wäre, dass alle asserts immer wahr sind, wäre ihr Einsatz zur Optimierung nicht umstritten
- Wenn garantiert wäre, dass Tests alle falschen asserts finden, wären auch Produktionsoptimierungen sicher
- In der Praxis kann man jedoch falsche asserts schreiben, und Tests decken sie nicht zwingend auf
- Wer asserts in Produktion abschaltet, verliert die Chance, falsche asserts möglichst früh zu entdecken
- Noch gravierender ist, dass späterer Code weiterhin unter Abhängigkeit von diesen falschen asserts geschrieben wird
- Im Beispielcode wird per assert angenommen, dass
processThingnur mit einem bereits gestartetenthingaufgerufen werden darffn processThing(thing: Thing) void { // this function must always be invoked on // a thing that has already been started assert(thing.is_started); // ... } - Dieses assert kann in Tests niemals fehlschlagen, während es in Produktion deaktiviert ist und dort tatsächlich falsch werden könnte
- Solange kein für Nutzer sichtbares Fehlverhalten auftritt, scheint alles in Ordnung zu sein, und die Entwicklung geht weiter
- Später könnte jemand Code hinzufügen, weil
thingja bereits gestartet sei und man daherbazohne zusätzliche Vorbereitung aufrufen könnefn processThing(thing: Thing) void { // this function must always be invoked on // a thing that has already been started assert(thing.is_started); // ... // Since thing is already started, we don't // need to foo the bar before bazzing the qux. // It would be really bad to baz the qux otherwise, // so we add an assert for good measure. assert(thing.is_fooed); thing.baz(qux); } - Auch wenn das zweite assert logisch korrekt sein mag, entsteht ein Risiko, wenn das erste assert in Wirklichkeit falsch werden kann
- In Tests schlägt das erste assert nicht fehl, also auch das zweite nicht
- In Produktion sind asserts deaktiviert, sodass der Moment, in dem die Schwachstelle in die Codebasis gelangt, unbemerkt bleiben kann
- Wenn asserts im Code Entwickler in die Irre führen, wird es unangemessen schwer, korrekten Code zu schreiben
Die Wahl hängt von den Prioritäten des Programms ab
- Jedes Programm hat andere Prioritäten, und bei manchen ist es legitim, Performance höher zu gewichten als die Minimierung von Fehlverhaltensrisiken
- In diesem Fall ist es naheliegend, asserts in Optimierungsmöglichkeiten umzuwandeln
- Das träge, gewohnheitsmäßige Deaktivieren von asserts in Produktion wird als schlechtere Wahl bewertet als sowohl eingeschaltete asserts als auch die aktive Nutzung von Performance-Optimierungen
- Eine Haltung, die ReleaseFast stark kritisiert, aber das Deaktivieren von asserts kritiklos hinnimmt, ist widersprüchlich
- Zine ist ein statischer Site-Generator und wird derzeit vor allem zum Bauen persönlicher Blogs genutzt
- Das Bedrohungsmodell ist nicht definiert, und das ist auch nicht die höchste Priorität
- Daher werden ReleaseFast-Builds ausgeliefert, weil sie etwa eine Größenordnung schneller laufen als Hugo
- Awebo ist eine selbst hostbare Discord-Alternative im Pre-Alpha-Stadium
- Es ist bereits klar, dass die Software personenbezogene Daten verarbeitet und dem Internet ausgesetzt sein wird
- Deshalb sollen zur Auslieferung ReleaseSafe-Builds bereitgestellt werden
- Einige zentrale Abhängigkeiten wie FFmpeg, Xiph Opus und SQLite sollen jedoch in ReleaseFast gebaut werden
- Dort wird der Performancegewinn klar höher bewertet als die weitere Reduktion des Risikos von Fehlverhalten
Entscheidungen realer Projekte und Sicherheitsfälle
- TigerBeetle ist eine Finanzdatenbank und lässt asserts immer eingeschaltet
- Ghostty ist ein Terminal-Emulator und liefert für macOS ReleaseFast-Builds aus
- Dasselbe Vorgehen wird auch Downstream-Nutzern empfohlen, etwa Maintainers von Linux-Distributionen, wie hier dokumentiert
- Zwei öffentlich bekannte, vergleichsweise schwerwiegende CVEs in Ghostty ermöglichten beide die Ausführung beliebiger Befehle ohne Speicherbeschädigung
- Speicherbeschädigung oder UIB sind nicht die einzigen Risiken
Implizite asserts, die in Zig nicht vollständig verschwinden
- Auch wenn sich eigene asserts deaktivieren lassen, können die impliziten asserts, die Zig selbst dem Code hinzufügt, nicht abgeschaltet werden
- Dazu gehören Integer-Überlauf, Division durch null oder Zugriffe außerhalb von Array-Grenzen
- Solche Bedingungen lösen entweder zur Laufzeit einen panic aus oder werden für Optimierungen genutzt
- Die Praxis, Produktions-asserts zu deaktivieren, kann dazu führen, dass falsche asserts in der Codebasis verrotten und sich vermehren
- Das verstärkt am Ende die Paranoia gegenüber UIB, und Entwickler könnten unbewusst Angst davor entwickeln, asserts wieder einzuschalten und sich den Konsequenzen zu stellen
- Die unausweichliche Schlussfolgerung lautet daher nicht, asserts abzuschalten und zu überdecken, sondern falsche asserts zu korrigieren
- Korrektheit sollte für das gesamte Programm angestrebt werden, nicht nur für irgendeine Teilmenge davon
1 Kommentare
Lobste.rs-Kommentare
Ich stimme zu, dass es bei
assertim Allgemeinen am besten ist, einfach zum Absturz zu bringen oder wie bei Rust nur die jeweilige Aufgabe abstürzen zu lassen. Aber ich finde es schwer zuzustimmen, dass die Nutzung vonassertals Optimierungs-Hinweis immer besser ist, als es einfach wegzulassen.Erstens helfen beliebige
asserts der Optimierung oft kaum, und viele Bedingungen kann der Optimierer nicht direkt verwerten. Wenn man nicht eine direkte Annahme wie „Dieser Zweig wird niemals erreicht“ einfügt, ist der Performancegewinn vermutlich nicht groß, wenn man überall im Code zufällige Annahmen verstreut.Zweitens vergrößert sich der Schadensradius von Fehlern erheblich, wenn man
assertin Annahmen umwandelt. Nehmen wir zum Beispiel ein System, das Daten verarbeitet, die nach Projekt oder Nutzer getrennt sind, und mitten in einer Berechnungsfunktion steht einassert, das einen Zustand abfängt, der eigentlich unmöglich sein sollte. Wenn man ihn im Release-Build wegen der Kosten deaktiviert, dann bleibt der Schaden bei bloßer Deaktivierung auf ein Projekt oder einen Nutzer begrenzt und wird vielleicht bei einer späteren Prüfung noch erkannt. Macht man daraus dagegen undefiniertes Verhalten, kann die Berechnung an völlig falschen Code springen, den Speicher beliebig beschädigen und die Daten aller Projekte zerstören.Wenn man also unsichere
asserts zum Standard im Release-Build macht, optimiert man letztlich voreilig beliebige Stellen des Codes und verringert dafür die Chance, Schäden im Problemfall zu lokalisieren. Ich finde, Rust ist hier gut designt:assert!()führt immer zu einem Panic,debug_assert!()nur im Debug-Modus, undassert_unchecked()führt im Debug-Modus zu einem Panic und wird im Release-Modus zu einem Optimierungs-Hinweis.ReleaseFastlieberReleaseSafeverwenden.asserts zu deaktivieren, sondern dagegen, sie wie eine allgemeine empfohlene Praxis pauschal abzuschalten.Wenn man entscheidet, dass der Performance-Einfluss zu groß ist, um sie im Release-Build beizubehalten, ist das völlig vernünftig. Außerdem führen teure
asserts, wie oben gesagt, mit hoher Wahrscheinlichkeit ohnehin nicht zu spürbaren Performanceverbesserungen.Auch in Zine gibt es einige solche Beispiele:
https://github.com/kristoff-it/zine/…
https://github.com/kristoff-it/zine/…
Zig hat keinen „standardmäßigen Release-Modus“. Man muss immer selbst wählen, wie
assertbehandelt werden soll, und die globalen Optionen sind Absturz oder Optimierung; keine von beiden kann wirklich als der eigentliche Standard gelten.Dass die beiden bislang veröffentlichten vergleichsweise schwerwiegenden CVEs in Ghostty beide ohne Speicherbeschädigung zu beliebiger Befehlsausführung führten, wirkt auf mich sehr merkwürdig. Dass das trotz Verteilung mit ReleaseFast passiert ist, widerspricht meinem Verständnis davon, wie die Welt funktioniert.
Aus meiner Erfahrung mit Terminalemulatoren sind diese Schwachstellen genau die erwartbare Art von lästigen Problemen. Das soll Entwickler oder Forscher nicht herabsetzen, aber solche Command-Injection an unerwarteten Stellen gehört in diesem Bereich fast schon dazu, ähnlich wie in anderen Domänen andere Injection-Schwachstellen häufig dazugehören.
Ich finde es lustig, dass ich seit fast 40 Jahren höre, man solle
asserts und Grenzprüfungen in Produktion „aus Performancegründen“ abschalten. In dieser Zeit sind Computer um mehrere Größenordnungen schneller geworden, und Software ist viel tiefer in das Leben aller Menschen eingedrungen, daher ist die Korrektheit zur Laufzeit heute wichtiger denn je.Konstruktiver gesprochen: Beim alten Microsoft gab es neben gewöhnlichen
asserts undchecks etwas, das ich anderswo kaum gesehen habe: Reporting-Assertions. Die verwendet man, wenn es Bedingungen gibt, die man nicht vollständig kontrolliert, von denen man aber annimmt, dass sie wahr sind, die man im falschen Fall defensiv behandelt und bei denen man per Logging oder Telemetrie wissen möchte, ob sie in der Praxis tatsächlich falsch werden. Zum Beispiel, wenn man annimmt, dass Nutzer nie mehr als 1000 Einträge in eine Liste packen und deshalb einen quadratischen Algorithmus verwendet, oder wenn man davon ausgeht, dass die Netzwerklatenz unter 200 ms liegt und deshalb ein Protokoll mit vielen Roundtrips einsetzt.check?Als einer der hier verlinkten Beteiligten kann ich sagen: Das macht aus meiner Sicht auf
asserteine lächerliche falsche Dichotomie und eine Karikatur. Wie ich auch in einem anderen Kommentar geschrieben habe, entscheide ich lieber proassert, ob in undefiniertes Verhalten überführt werden soll. Meine Kritik an ReleaseFast ist, dass diese Entscheidung nicht nur für alleasserts in einem bestimmten Bereich, sondern auch mit sämtlichen Sicherheitsprüfungen zusammengebunden wird.Ich stimme kristoff zu, dass es dumm ist, nicht korrigierte
asserts nur deshalb abzuschalten, weil sie Abstürze verursachen. Aber ich stimme nicht zu, dass „Absturz oder undefiniertes Verhalten“ die einzigen vernünftigen Alternativen seien. Die Position von goldstein im Schwesterkommentar liegt meiner Ansicht nach näher an dem, was ich denke.Es ist schwer zu verteidigen, das Verhalten von
assert_unchecked()zum globalen Standard zu machen, aber als Performance-Optimierung kann es sinnvoll sein. Wenn ein Produktions-Build deutlich schneller wird, sobald man alleasserts in Annahmen umwandelt, dann könnte es eine kleine Zahl von Annahmen, hoffentlich sogar nur eine einzige, geben, die den Großteil der Verbesserung ausmacht, und man könnte sie mit Methoden wie binärer Suche finden.ReleaseSafeundReleaseFast/ReleaseSmall.In der Programmanalyse-Literatur gibt es eine Dualität, die Behauptungen im Code oder
assertin zwei Formen einteilt. Die eine betrifft den Kontext um den Code herum, bei einer Funktion also die Bedingung, die der Aufrufer erfüllen muss, und die andere den Code selbst, bei einer Funktion also die Bedingung, die die Funktion erfüllen muss.Diese Unterscheidung wird klar, wenn man sie unter dem in der Vertrags- und Gradual-Typing-Literatur üblichen akademischen Begriff der „Verantwortung“ (blame) betrachtet. Wenn eine Behauptung über den Kontext fehlschlägt, liegt der Fehler nicht bei uns, sondern die Verantwortung beim Kontext oder Aufrufer; es kann aber auch sein, dass der Aufrufer korrekt ist und die Behauptung selbst fehlerhaft ist. Wenn eine Behauptung über den Code selbst fehlschlägt, liegt die Verantwortung bei uns; es kann aber auch sein, dass der Code korrekt ist und die Behauptung selbst fehlerhaft ist.
Auf Funktionsebene ist eine Vorbedingung eine Behauptung über den Kontext und eine Nachbedingung eine Behauptung über den Code selbst. Beide können allerdings auch mitten im Code platziert werden. Manche Verifikations-Frameworks verwenden
assertfür Behauptungen über den Code undassumefür Behauptungen über den Kontext. Das hängt auch damit zusammen, wie einige Test-Frameworks, insbesondere Frameworks für zufällige Tests, dies interpretieren. Wennassertfehlschlägt, wird der Test als fehlgeschlagen markiert; wennassumefehlschlägt, wird der Test übersprungen.Das scheint auf Bun anzuspielen, deshalb würde ich die Verbindung gern etwas formeller festhalten. Es gibt ein Zig-Issue aus dem Jahr 2024, in dem Bun-Ersteller Jarred Sumner vorgeschlagen hat, dass
unreachablein ReleaseFast paniken sollte. Die Kommentare von Andrew Kelley und Matthew Lugg in diesem Thread sind für diese Diskussion relevant.=> https://github.com/ziglang/zig/issues/19664
Bun verwendet eigene
assert-Funktionen, die im Release-Modus paniken oder entfernt werden, aber kein undefiniertes Verhalten einführen. Man sollte allerdings auch Loris' Fußnote im Kopf behalten: „Als Sprache fügt Zig dem Code implizit vieleasserthinzu, die nicht deaktiviert werden können.“Ich möchte nicht zu lange über Bun sprechen. Es ist schließlich ein einzelnes Projekt eines kleinen Teams. Der Kernpunkt ist: Wenn auch nur die geringste Sorge besteht, sollte man ReleaseSafe verwenden. ReleaseSafe hat zwar den Ruf, langsam zu sein, aber in meinen kleinen Zig-Projekten konnte ich keinen Benchmark-Unterschied zwischen ReleaseSafe und ReleaseFast messen. Wahrscheinlich ist es trotzdem noch schneller als viele andere Sprachen.
Oder man liefert, wenn es im Kontext sinnvoll ist, zunächst ein ReleaseFast-Binary aus und wechselt zurück zu ReleaseSafe, sobald wegen undefiniertem Verhalten nichtdeterministische Bugreports eingehen. Dann kann man verwertbare Bugreports sammeln, etwa darüber, welches
assertfehlgeschlagen ist, oder über Out-of-Bounds-Zugriffe und Overflows, und den Code beheben. Ich würde diesen Ansatz sogar empfehlen, wenn man sich ursprünglich in einem Kontext, in dem man gar nicht mit ReleaseFast hätte ausliefern sollen, dennoch dafür entschieden hat :^)Man kann auch Abhängigkeiten anpassen und mit
@setRuntimeSafetydasselbe Vorgehen nur auf Teile eines Projekts anwenden. Letztlich sind alle nötigen Werkzeuge vorhanden, wenn man bereit ist, klug damit umzugehen.Man sollte nicht so schreiben, als dürften in
assert-Aufrufen Ausdrücke mit Seiteneffekten stehen. Das ist eine schlechte Praxis. Auchassertfür Fehlerprüfungen zu verwenden, sollte man vermeiden. Fairerweise scheint der Autor das aber nicht zu behaupten.Umgekehrt wird auch erklärt, dass, wenn
assertvon aufwendigen Berechnungen abhängt, diese Berechnungen im unchecked-Modus nicht zwingend entfernt werden und man sie deshalb mitcomptime ifabsichern sollte.Hoffentlich ist dem Autor die Ironie der Aussage nicht entgangen, dies sei „eine gute Gelegenheit, das von Makros hinterlassene Trauma loszulassen und Einfachheit zu akzeptieren“. Gemeint ist offenbar, man solle „die Einfachheit akzeptieren, den Build-Modus des Programms zu berücksichtigen und überall defensive
comptime ifzu verstreuen“.Ich schreibe gelegentlich numerischen Code in C# und verwende viele
assert, die in Release deaktiviert werden. Sie sind zu teuer, um sie in jeder dicht laufenden Schleife auszuführen, aber in Unit-Tests ist es nützlich, wenn eine Routine sofort abbricht, sobald sie zum ersten Mal NaN-Eingaben sieht.Solche NaNs entstehen oft nicht durch Benutzereingaben, sondern durch Bugs im Code, etwa wenn der Optimierer an Stellen gerät, an denen er nicht sein sollte, oder wenn bessere Randbedingungen nötig sind. Natürlich müssen Benutzereingaben womöglich validiert werden, aber das sollte an der äußersten Grenze geschehen, nicht tief im Algorithmus. Es wäre schön, ein Beweissystem zu haben, das als Ergebnis der Validierung von Benutzereingaben Invarianten im Inneren des Algorithmus auch ohne
assertbeweisen kann, aber das ist ein Nebenprojekt, und wenn es abstürzt, stirbt niemand.90 % der Meinungsverschiedenheiten über
assertentstehen dadurch, dass das Wort schlecht definiert ist und für mehrere Dinge verwendet wird; dadurch werden Denken und Kommunikation unscharf. Deshalb sollte man das Konzept in die folgenden drei Begriffe aufteilen und diese strikt verwenden.assert(bool)oder in Rustassert_unchecked()ist etwas, das der Programmierer für immer wahr hält und das der Compiler ebenfalls als immer wahr annimmt und für Optimierungen nutzt. Um Assoziationen mit prüfenden Assertions älterer Sprachen zu vermeiden, wäreassume()vielleicht die bessere Bezeichnung.check(bool)bedeutet: Wenn die Bedingung falsch ist, wird gepanikt, und wenn sie wahr ist, läuft das Programm weiter; und genau so verhält es sich immer.debug_check(bool)entspricht im Debug-Moduscheck()und läuft im Release-Modus immer weiter. In der Praxis wird das über ein Flag--debug_checksgesteuert, das im Debug-Modus standardmäßig aktiviert ist.Dazu braucht es auch ein Compiler-Flag
--check_asserts, dasassert()incheck()umwandelt. Das verwendet man, wenn man die eigenenassertanzweifelt und sie verifizieren möchte; im Debug-Modus ist es standardmäßig aktiviert. Solange man nicht ganz klar sagt, was man mit „assert“ meint, ist eine reife Diskussion unmöglich, und man verschwendet nur Worte.