2 Punkte von GN⁺ 2025-01-13 | 1 Kommentare | Auf WhatsApp teilen
  • Selbst bis C23 hat sich Standard-C weiterentwickelt, aber Walter Bright meint, dass alte Unbequemlichkeiten wie Auswertung konstanter Ausdrücke, Vorwärtsreferenzen und Header-Abhängigkeiten den Entwicklungsfluss weiterhin behindern
  • ImportC, der in den D-Compiler eingebaute C-Compiler, nutzt den Vorteil einer neuen Implementierung und kann Funktionen, die die Bedingungen erfüllen, an constant-expression-Positionen zur Compile-Zeit ausführen
  • Wenn CTFE möglich wird, können Unit-Tests wie _Static_assert(sum(3, 4) == 7,...) bei jedem Kompilieren ohne separate ausführbare Datei ausgeführt werden
  • Die Beschränkung der Deklarationsreihenfolge in Standard-C führt zu wiederholten Vorwärtsdeklarationen und umgekehrt angeordnetem Code, aber ImportC wählt einen Ansatz, der weniger an die Reihenfolge globaler Deklarationen gebunden ist
  • Wenn man Deklarationen aus einer .c-Datei wie mit __import dex; direkt importiert, kann man separate .h-Dateien reduzieren und auch den Debugging-Aufwand durch Abweichungen zwischen Header und Implementierung senken

Verbliebene Unbequemlichkeiten in Standard-C

  • Standard-C wurde bis C23 regelmäßig verbessert, aber Walter Bright sieht weiterhin Bereiche, die noch nicht behoben wurden
  • Die Dlang-Community hat einen C-Compiler in den Compiler der Programmiersprache D eingebettet, und dieser Compiler wird ImportC genannt
  • ImportC wurde von Grund auf neu entwickelt, sodass es Spielraum gab, mit moderner Compilertechnik Schwächen von C anzugehen
  • Im Artikel werden vier Punkte behandelt
    • Auswertung von constant-expression
    • Unit-Tests zur Compile-Zeit
    • Vorwärtsreferenzen von Deklarationen
    • Import von Deklarationen

Funktionsausführung zur Compile-Zeit und Auswertung konstanter Ausdrücke

  • C kann einfache Ausdrücke per constant folding zur Compile-Zeit berechnen, aber in Standard-C können Funktionen nicht zur Compile-Zeit ausgeführt werden
  • Wenn man den Beispielcode enum E { A = 3, B = 4, C = sum(5, 6) }; mit gcc kompiliert, erscheint ein Fehler, dass der Enumeratorwert von C keine ganzzahlige Konstante ist
  • ImportC kann denselben Code kompilieren
  • Die Richtung ist, dass der Compiler an allen Stellen der C-Syntax, an denen constant-expression vorkommt, Funktionen zur Compile-Zeit ausführen können sollte
  • Die betreffende Funktion darf allerdings keine Operationen wie I/O, Zugriff auf veränderliche globale Variablen oder Systemaufrufe ausführen

Unit-Tests, die bei jedem Kompilieren laufen

  • Wenn CTFE als Compile-Zeit-Auswertung von Funktionen möglich wird, kann sich auch die Art von Unit-Tests ändern
  • Dass Unit-Tests in C-Code selten sind, liegt an der Umständlichkeit, separate Build-Ziele und ausführbare Dateien zu erstellen und auszuführen
  • Wenn man den Beispielcode _Static_assert(sum(3, 4) == 7, "test #1"); mit gcc kompiliert, tritt ein Fehler auf, dass der Ausdruck in der statischen Assertion keine Konstante ist
  • ImportC kann diesen Code kompilieren
  • Dieser Ansatz vereinfacht Unit-Tests für Funktionen, die zur Compile-Zeit ausgeführt werden können
    • Kein separater Build erforderlich
    • Keine zusätzlichen Arbeitsschritte erforderlich
    • Die Tests laufen jedes Mal, wenn der Code kompiliert wird
  • Auch in der ImportC-Testsuite wird dieser Ansatz breit eingesetzt

Durch Vorwärtsreferenzen entstehende Wiederholung von Deklarationen

  • In Standard-C sind Vorwärtsreferenzen nicht erlaubt, weil der Compiler nur Deklarationen kennt, die lexikalisch vorher erscheinen
  • Wenn im Beispielcode floo() zuerst das weiter hinten definierte dex() aufruft, meldet gcc einen Fehler, dass die implizite Deklaration von dex mit dem Typ der tatsächlichen Definition kollidiert
  • Vertauscht man die Reihenfolge von floo und dex, wird derselbe Code korrekt kompiliert
  • ImportC kann globale Deklarationen unabhängig von ihrer Reihenfolge akzeptieren
  • Wenn Vorwärtsreferenzen nicht möglich sind, muss für jede spätere Definition eine separate Deklaration hinzugefügt werden
    • Zuerst schreibt man eine Deklaration wie char dex(char *s, int i);
    • Danach muss die Funktionsdefinition erneut geschrieben werden
  • Diese Vorgehensweise ist unnötige Wiederholungsarbeit und führt dazu, dass Programmierer leaf-Funktionen oben und globale Interface-Funktionen unten platzieren, also in umgekehrter Reihenfolge
  • Das wird kritisiert, weil es so sei, als würde man einen Zeitungsartikel von unten nach oben lesen

Deklarationen ohne Header-Dateien importieren

  • Um in C externe Module zu verwenden, erstellt man normalerweise Deklarationen in einer .h-Datei und bindet sie in der .c-Datei per include ein
  • Die Beispielstruktur ist wie folgt
    • floo.c inkludiert dex.h und ruft dex() auf
    • dex.h deklariert char dex(char *s, int i);
    • dex.c inkludiert dex.h und definiert dex()
  • Für jedes externe Modul eine .h-Datei zu erstellen, erhöht den Wiederholungsaufwand
  • Wenn die .h-Datei nicht exakt mit der .c-Datei übereinstimmt, kann es viel Zeit kosten herauszufinden, was falsch ist
  • Der vorgeschlagene Ansatz ist, dex.c direkt zu importieren
    • In floo.c wird __import dex; verwendet
    • In dex.c steht nur eine Definition wie char dexx(char *s, int i) { return s[i]; }
  • Auf diese Weise muss überhaupt keine .h-Datei mehr geschrieben werden
  • ImportC unterstützt auch diesen Ansatz

Verwandte Dokumente

1 Kommentare

 
GN⁺ 2025-01-13
Meinungen auf Hacker News
  • Eines der Dinge, die ich am meisten vermisse, wenn ich eine andere Sprache als C verwende, sind Header-Dateien.
    Mir gefällt an C-Code besonders, wie klar öffentlich/privat und Interface/Implementierung getrennt sind. Es ist angenehm, dass man schon durch Überfliegen der .h-Datei einer Library erkennen kann, wie sie zu benutzen ist; meist steht die Nutzungsdokumentation in der .h und wird nicht mit der .c dupliziert. Man kann die Dokumentation auch in die .c schreiben, aber aus Sicht der Nutzer ist es dann deutlich weniger angenehm, das Interface zu lesen.

    • Diese Behauptung kam mir immer seltsam vor; andere Sprachen lösen dieses Problem mit Tools, und ich halte das für besser.
      In Rust reicht zum Beispiel cargo doc --open, um Interface und Verwendung einer Library anzusehen. Die gesamte öffentliche API wird automatisch in durchsuchbarer Form erzeugt, ohne Code zwischen Header und Source zu duplizieren oder manuell pflegen zu müssen.
    • Header-Dateien sind meiner Ansicht nach eher ein schwacher Hack, um mit ressourcenbeschränkten Plattformen der 70er-Jahre zurechtzukommen.
      Sie funktionieren nur dann richtig, wenn man sich an Konventionen hält, und wirken im Vergleich zu Sprachen wie Ada, in denen Interface- und Implementierungsspezifikationen gut entworfen sind und kein wiederholtes Parsen nötig ist, ziemlich kümmerlich. Ich mag es, C zu verwenden, aber dieser Teil hätte besser entworfen werden müssen.
    • Ich stimme zu und zugleich nicht.
      C-Header-Dateien fügen Text schlicht unverändert ein und sind damit eher auf dem Niveau von „scheint irgendwie zu funktionieren“. Ada dagegen hatte Konzepte von Packages und Package Bodys; das Package entsprach der Header-Datei, der Package Body der Implementierung. Als ich vor langer Zeit Ada benutzt habe, konnten alle auf Basis des Packages kompilieren, selbst wenn die Implementierung des Package Bodys noch nicht fertig war, sodass man das Interface schon vor der Implementierung abstimmen konnte. In eine andere Richtung gefällt mir Pythons import als eine Art „Header-Datei“, weil es sich natürlich auf das Dateisystem abbildet und man sich nicht mit der Include-Semantik von C befassen muss.
    • Dahinter scheint ein Unterschied in der Denkweise zu liegen.
      Wenn es Header-Dateien gibt, denkt man über das Interface als etwas anderes als die Implementierung nach. Deshalb empfinden Menschen, die an diesen Ansatz gewöhnt sind, die Vorstellung als unangenehm, dass ein Interface von einem Tool generiert wird. Ein Interface ist kein Nebenprodukt der Implementierung, sondern etwas separat und bewusst Entworfenes, und für manche ist es wichtiger als die Implementierung. Umgekehrt empfinden Menschen, die an automatisch generierte Dokumentation gewöhnt sind, es als unangenehm, wenn das Interface nicht aus der Implementierungsquelle als einziger Source of Truth erzeugt wird. Wenn man lange mit Sprachen mit und ohne separate Interface-Dateien arbeitet, verhärtet man sich in seinem jeweiligen Lager und vergisst das Gefühl dafür, wie man in der anderen Richtung gedacht hat.
    • In den meisten kompilierten Modulsprachen ist das bereits möglich.
      Entweder separat wie in Modula-2, Modula-3, Ada, Standard ML, Caml Light, OCaml, F# und D, oder generierbar per Text- oder Grafik-Tool wie in Object Pascal, D, Haskell, Java, C#, F#, Swift, Go und Rust. Sie alle haben stärkere Typen, schnellere Kompilierung und richtige Namespaces. An den Toolchains von Rust und Swift gibt es noch einiges zu tun, aber C-Tools waren im Vergleich zu dem, was außerhalb von Bell Labs geschah, immer primitiv. Hätte AT&T das kommerziell nutzen können, wäre die Geschichte wohl anders verlaufen; so bekamen wir am Ende statt reifer Orangen kostenlose Zitronen. Trotzdem haben wir so etwas wie TypeScript für C gebaut, und heute unterstützt es auch Collection-Typen mit Bounds-Checking sowie richtige Module.
  • Da der Autor in diesem Bereich viel mehr Wissen und Erfahrung hat, würde mich interessieren, wie er die folgenden Probleme lösen würde.
    Konstantenausdruck-Auswertung wird einfacher, wenn man sie nur innerhalb einer Translation Unit vornimmt, aber dadurch wird stark eingeschränkt, was ohne Code-Wiederholung möglich ist. Compile-Time-Unit-Tests sind bis zu einem gewissen Grad möglich, wenn Tests als Makros ausgedrückt werden können, und mit dem vorherigen Punkt wird es einfacher. Vorwärtsreferenzen von Deklarationen könnten auf starken Widerstand stoßen, weil der Compiler von einem Pass auf zwei Pässe umgestellt würde und das Performance-Auswirkungen hätte. Für Leute, die riesige Codebases kompilieren und dabei sogar Translation Units parallelisieren, könnte das schwer akzeptabel sein. Deklarationen importieren ist eine Änderung, die die Kompatibilität bricht. Ich habe in C schon einmal nach einer Variablendefinition eine .c-Datei importiert, die Variable geändert und dieselbe .c erneut importiert, um etwas Template-Ähnliches zu implementieren. Ich habe auch vor die SQLite C Amalgamation mehrere Definitionen gesetzt und Funktionen hinzugefügt, um interne Funktionen offenzulegen; solche Anwendungsfälle würden wohl alle kaputtgehen. Mich würde interessieren, ob es für solche Probleme Lösungen gibt.

    • Es stimmt, dass der Compiler den Quellcode der auszuwertenden Funktion sehen können muss.
      In C geht das mit #include, und in D importiert man das Modul mit dem benötigten Code. Tests als Makros auszudrücken, passt nicht, wenn man Funktionen testen will. Das Beispiel war zur Veranschaulichung einfach gehalten; in der tatsächlichen Nutzung kann es deutlich komplexer werden. In puncto Performance kompiliert D schneller als C-Compiler, vor allem weil der C-Präprozessor ein fossiles Konstrukt ist, das mehrere Durchläufe erfordert, während D statt #include import verwendet und .h-Dateien nicht immer wieder neu kompiliert. Ds Strategie besteht darin, Parsing und semantische Analyse zu trennen; das kann zwar etwas langsamer sein, aber es fallen keine Kosten dafür an, doppelte Deklarationen erneut zu kompilieren und zusammenzufalten. Compile-Time Function Execution kann bei intensiver Nutzung zum Bottleneck werden, aber bei leichter Nutzung ist die Performance in Ordnung. Wenn man Templates in C per Hack implementiert, ist man bereits über die Sprache hinausgegangen und braucht eine mächtigere Sprache. D besitzt Metaprogramming erster Klasse, und andere Template-Sprachen folgen häufig dem Weg, den D vorgezeichnet hat.
    • Persönlich mag ich Vorwärtsreferenzen nicht, weil sie das Lesen von Code erschweren.
      Man kann dann nicht mehr darauf vertrauen, dass der Abhängigkeitsgraph in topologisch sortierter Reihenfolge vorliegt.
    • Andere Sprachen scheinen auch ohne Header-Dateien oder Forward Declarations gut zurechtzukommen; daher fällt es mir schwer zu verstehen, warum es Widerstand gibt.
      Mich würde auch interessieren, ob moderne C-Compiler tatsächlich immer noch Single-Pass-Compiler sind.
  • Das Beispiel zur Auswertung konstanter Ausdrücke im Artikel ist zwar ziemlich einfach, aber in komplexeren Fällen dürften Compiler-Geschwindigkeit und Speicherverbrauch deutlich schlechter werden, und um das sinnvoll zu nutzen, bräuchte man wohl eine virtuelle Maschine.
    Daher kann ich nachvollziehen, dass man es für „zu komplex“ hielt, um es in den Standard aufzunehmen. Ich hätte mir gewünscht, dass C++ oder C statt der in C++20 definierten seltsamen Mischung eher in Richtung Deklarations-Import gegangen wären. Zum Beispiel so, dass man mit #import "string.c" as str ein Modul unter einem Symbol importiert und dann auf alle nicht-statischen Symbole der Datei wie str.trim(" Hello World "); zugreifen kann. Unabhängig davon gefällt mir etwas wie __import dex;, bei dem der Dateipfad nicht explizit ist, nicht. In diesem Fall weiß man nicht, ob dex.d oder dex.c importiert wird

    • Andere populäre Sprachen können das ebenfalls, und in D ist Compile-Time Function Execution eine sehr beliebte und nützliche Funktion.
      Wenn man sie häufig nutzt, kostet das natürlich Compile-Zeit und Speicher. Was die virtuelle Maschine angeht: Constant Folding selbst ist bereits eine Art virtuelle Maschine, und hier würde man sie nur um die Behandlung von Funktionsaufrufen erweitern. Die Semantik von C ist einfach, daher ist das nicht so schlimm. Der vorgeschlagene Import-Ansatz entspricht ziemlich genau dem, was D-Imports tun: https://dlang.org/spec/module.html#import-declaration. Das Problem, ob es dex.d oder dex.c ist, tritt tatsächlich auf; die Antwort darauf ist die Konfiguration der Import-Pfade. Ähnlich wie die Include-Pfade eines C-Compilers
    • Die meisten realen C-Compiler machen zur Optimierung vermutlich ohnehin schon in gewissem Maß Compile-Time Evaluation.
      In C gibt es bereits konstante Ausdrücke. Das größere Hindernis ist, dass der Compiler Zugriff auf den Quellcode der Funktion haben muss, weshalb es wahrscheinlich auf Funktionen in derselben Übersetzungseinheit beschränkt würde. Außerdem könnte das menschliche Problem noch viel größer sein: Ein Komitee mit Vertretern mehrerer Compiler müsste sich auf die Semantik einer solchen Konstantenauswertung einigen
    • Das wirkt tatsächlich fast 1:1 wie C++-constexpr.
      Da fast alle C-Compiler ohnehin auch C++-Compiler sind, frage ich mich, ob es wirklich so schlimm wäre, in C constexpr-Funktionen und -Auswertung zu unterstützen
  • Ich schreibe für C-Code immer Unit-Tests.
    Mit einem guten Build-System und etwas Boilerplate ist das nicht schwer. Die Tests der npy-Bibliothek rufen in test_load_uint8() npy_load("tests/npy/uint8.npy") auf, prüfen Dimensionen, Größe und Typ per assert, geben anschließend mit npy_free frei und werden in main etwa mit PRINT_RUN(test_load_uint8); ausgeführt. Man könnte Teile der Tests auch per Präprozessor erzeugen, aber ich halte es lieber einfach

    • Diese Funktion sieht so aus, als würde sie I/O machen, daher funktioniert sie nicht als Compile-Time-Test.
      Beispiele für Unit-Tests des ImportC-Compilers sehen etwa so aus: _Static_assert(sizeof(struct S22079){1,2,3} == sizeof(int)*3, "ok"); und _Static_assert(sizeof(struct S22079){1,2,3}.a == sizeof(int), "ok");. Da die Semantik zur Compile-Zeit geprüft wird, muss man nicht linken und ausführen. Je mehr Tests es gibt, desto schneller wird dieser Ansatz insgesamt, und je schneller die Testsuite läuft, desto höher ist die Produktivität
    • Ich mache es fast genauso.
      Früher habe ich mich einmal tief in schicke Unit-Test-Frameworks eingearbeitet, aber nachdem ich erkannt hatte, dass sie kaum Vorteile bringen, bin ich bei fast derselben Form gelandet. Was andere vielleicht kritisch sehen: Ich bin bereit, die zu testende .c-Datei per #include einzubinden, um statische Funktionen aufzurufen. Es wird genau eine .c-Datei eingebunden. Außerdem mache ich vor dem #include noch eine kleine Präprozessor-Behandlung, damit NDEBUG nicht definiert ist, falls jemand im „Release-Modus“ baut und dadurch assert deaktiviert würde
    • Tests so einfach wie möglich ausführbar zu machen, hilft meiner Meinung nach enorm dabei, zu einer Codebasis mit echten Tests zu kommen.
      Ich verwende etwas Ähnliches: https://github.com/ensisoft/detonator/blob/master/base/test_.... Es hat viel von boost.test.minimal übernommen und war ursprünglich ein einzelner Header, aber im Lauf der Jahre musste eine einzelne Übersetzungseinheit hinzugefügt werden. Meine Schlussfolgerung ist: Wenn man die Codebasis in einem Zustand hält, in dem die Tests immer bestehen, braucht man deutlich weniger Komplexität bei Fehlermeldungen oder Fehlertoleranz der Testwerkzeuge
  • Compile-Time-Unit-Tests sind eine ebenso schlechte Idee wie „ungenutzte Imports/Variablen/Ergebnisse“ zu Fehlern statt Warnungen zu machen.
    Das ist eine „Kindermädchen-Funktion“, die Entwicklern Kontrolle wegnimmt und sie am Ende zwingt, bürokratische Schritte abzuarbeiten, um die Arbeit zu erledigen. Solche Build-fehlschlagenden Tests sind gut für Builds, bei denen man denkt: „Jetzt ist es fertig“, aber nicht für die „noch in Arbeit“-Builds, die 99 % aller Builds ausmachen. Es ist, als würde man sagen: „Du darfst die Tischsäge nicht benutzen, bevor du die Bohrmaschine weggeräumt hast.“

    • Dem kann ich nur schwer zustimmen.
      Wenn man versucht hat, einen Gedanken auszudrücken, und ein Compile-Time-Test meldet, dass er falsch ist, dann ist dieser Gedanke vielleicht tatsächlich noch unvollständig, oder man hat nicht alle Folgen dieser Ausdrucksweise ausreichend bedacht. Das ähnelt der Typprüfung in Haskell. Ein Programm, das die Typprüfung nicht besteht, kann nicht kompiliert werden, und der Programmierer wird gezwungen, immer nur vollständige Gedanken auszudrücken. Theoretisch kann das zu besser durchdachten Programmen führen. Allerdings macht es das Schreiben schwieriger, weil der Programmierer auch die Ecken finden muss, von denen er weiß, dass sie „ungültig sind, aber ihn nicht interessieren“
    • Solche Compile-Time-Tests könnten eher static_assert ähneln und sind sehr wertvoll, um inkompatible Verwendungen von Bibliotheksfunktionen aufzudecken.
      Ich halte das für eine ziemlich gute Idee
  • „Dass Leaf-Funktionen zuerst kommen und globale Interface-Funktionen zuletzt“, ist für mich genau verkehrt herum
    Aus mehreren Gründen bevorzuge ich es, Code in topologisch sortierter Reihenfolge zu schreiben. Das ähnelt der Art, wie man Code innerhalb einer Funktion schreibt, macht klar, wo Funktionen innerhalb eines Moduls stehen sollten, und vor allem werden zyklische Abhängigkeiten zwischen internen Code-Teilen eines Moduls sehr deutlich. Zyklische Abhängigkeiten mag ich nicht besonders, weil sie eine Codebasis stärker verflechten und es erschweren, ein Modul als eigenständige Einheit zu verstehen. In Python können sie außerdem Probleme erzeugen, die erst zur Laufzeit sichtbar werden[0], und zyklische Imports sind so verbreitet, dass aktuelle Type-Checker diese Diagnose offenbar standardmäßig deaktivieren[1]. Sprachen wie C, OCaml und SML, die keine Vorwärtsreferenzen unterstützen, erlauben es, bei zyklischen Abhängigkeiten das „Prinzip der geringsten Überraschung“ anzuwenden. OCaml verbietet sogar rekursive Abhängigkeiten zwischen Funktionen, sofern man sie nicht etwa als let rec fn1 = .. and fn2 = .. deklariert; beim Schreiben ist das etwas lästig, beim Lesen aber eine wichtige Information
    [0]: https://gist.github.com/Mark24Code/2073470277437f2241033c200...
    [1]: https://microsoft.github.io/pyright/#/configuration?id=type-... (siehe reportImportCycles)

  • Es gibt die Erklärung, dass der Compiler nur lexikalisch Vorhergehendes kennt und sich dadurch eine „von unten nach oben“-Reihenfolge ergibt, bei der Leaf-Funktionen zuerst und globale Interface-Funktionen zuletzt kommen. Aber selbst in Sprachen wie Python, die Vorwärtsreferenzen erlauben, ist diese Reihenfolge verbreitet[0]
    Ich frage mich, ob das ein Überbleibsel aus Sprachen ohne Vorwärtsreferenzen ist oder ob es für bestimmte Arten von Code tatsächlich sinnvoller ist
    [0] https://stackoverflow.com/a/73131538

    • Python hat keine Vorwärtsreferenzen
      Bezeichner im Funktionskörper werden lediglich erst aufgelöst, wenn die Funktion selbst ausgeführt wird; zu diesem Zeitpunkt ist alles im Modul-Scope bereits definiert. Man kann das direkt überprüfen, indem man beliebige Namen in einen Funktionskörper schreibt und das Modul lädt
    • In meinem Code steht das öffentliche Interface tendenziell immer oben, Implementierungsdetails wandern ans Dateiende
      Es gibt keine strikte Regel, aber so liest es sich für mich sauberer und sinnvoller. Besonders wenn die Implementierung groß ist, möchte ich nicht erst durch alles hindurchscrollen müssen, bevor ich sehe, was sie grundsätzlich tut. Mich würde interessieren, wie andere das handhaben
  • Zu den „offensichtlichen Dingen“, die C leisten sollte, gehören Unterstützung für einen Slice-Typ, der Pointer und Länge enthält, reentrant-fähige und möglichst Thread-sichere Versionen von APIs, die globalen Zustand verwenden, die Standardisierung von etwas wie defer in Go oder Zig oder dem cleanup-Attribut von GCC, sowie portable Unterstützung für Unicode und UTF-8

    • Sind die meisten davon nicht eher gewünschte Funktionen der Standardbibliothek statt Dinge, die die Sprache selbst leisten sollte?
  • Dass der Compiler überall in der C-Syntax, wo konstante Ausdrücke vorkommen, Funktionen zur Compile-Zeit ausführen können sollte, solange die Funktion kein I/O macht, nicht auf veränderliche globale Variablen zugreift, keine Systemaufrufe ausführt usw., lässt sich leicht aushebeln
    Man muss nur eine sehr lange laufende reine Funktion wählen. Wenn zum Beispiel int busybeaver(int n) {...} eine reine Funktion ist, die die maximale Laufzeit einer Busy-Beaver-Maschine mit n Zuständen zurückgibt, wird Code wie int x = busybeaver(99); zum Problem

  • C23 hat constexpr, aber es kann noch nicht auf Funktionen angewendet werden
    Es gibt allerdings einen Vorschlag dazu: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2976.pdf