4 Punkte von GN⁺ 2023-10-10 | 1 Kommentare | Auf WhatsApp teilen
  • Im Jahr 2023 hat sich meine Art, C-Code zu schreiben, stark verändert, und ich habe meinen Stil rund um kurze Typnamen, den Verzicht auf nullterminierte Strings, das Zurückgeben von Structs und das Kompilieren als einzelne Translation Unit neu geordnet
  • Kurze Aliase wie u8, i32, size und s8 sowie das Weglassen von const und struct sind eine bewusste Entscheidung, um visuelles Rauschen und kognitive Last bei wiederkehrenden Deklarationen zu reduzieren
  • Strings behandle ich nicht mehr als nullterminierte Zeichenketten, sondern als s8 Fat Pointer mit data und len; für Win32- und UTF-16-Umgebungen nutze ich zusätzlich c16 und s16
  • Beim Funktionsdesign bevorzuge ich Struct-Rückgaben gegenüber Out-Parametern und verwende ein Muster, bei dem ein nullinitialisierter Rückgabewert nur im Erfolgsfall ok gesetzt bekommt
  • Selbst bei Makros, assert, Win32-Deklarationen und Inline-Assembler lege ich Wert auf gut lesbare lokale Regeln, passe mich beim Beitragen zu anderen Projekten aber deren Stil an

Mit kurzen Typnamen Absicht ausdrücken

  • Für grundlegende Ganzzahl-, Zeichen- und Pointer-Größentypen verwende ich kurze Aliase
    • Beispiele: u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • Diese Namen kommen im ganzen Programm häufig vor, daher hat Kürze direkte Vorteile beim Lesen und Reviewen
  • Das Suffix _t verwende ich nicht mehr; inzwischen wirkt es auf mich visuell unruhig
  • Für signed-Typpräfixe bevorzuge ich i gegenüber s
    • s halte ich für String-Typnamen frei
  • Für Größentypen verwende ich size statt isize
    • weil ich signed size als wichtigeren Standard ansehe
    • usize nutze ich hauptsächlich in dem engeren Fall der Interaktion mit externen Interfaces
  • b32 macht die Absicht „32-Bit-Boolean“ deutlich
    • Statt _Bool verwende ich eine natürliche Wortbreite
    • In der Praxis liegt der Wert oft ohnehin in einem Register oder im Struct-Padding
    • Wenn Speicher wirklich wichtig ist, packe ich Booleans in eine Variable flags
  • c16 ist der Typ für UTF-16-Zeichen, den ich unter Win32 brauche
    • Auf Basis von char16_t hilft das Debuggern wie GDB, die Daten als Zeichen darzustellen
    • Der offizielle Win32-Typname ist wchar_t, aber ich bevorzuge eine explizite UTF-16-Bezeichnung
  • u8 verwende ich für Oktette und meist für UTF-8-Daten, byte dagegen für rohen Speicher und als spezieller Aliasing-Typ
  • Sorgen über Systeme ohne Unterstützung für Fixed-Width-Typen halte ich für praktisch wenig relevant
    • Auch lange Typnamen wie int_fast32_t empfinde ich als unnötige Verschwendung
  • Wenn ich nur Codeausschnitte separat zeige, verwende ich diese Aliase nicht isoliert
    • Denn zum Verständnis brauchen Leser auch die zugehörigen typedefs

Regeln für Makros und assert

  • Funktionsartige Makros schreibe ich klein
    • Beispiele: countof(a), lengthof(s), new(a, t, n)
  • Für Konstanten bevorzuge ich weiterhin ALL_CAPS, aber bei funktionsartigen Makros halte ich Kleinschreibung für besser lesbar
  • Funktionsartige Makros haben weniger Namespace-Probleme als allgemeine Makros
    • Man kann ein Makro new() und zugleich eine Variable oder ein Feld new haben
    • Denn nur in Aufrufform wird es als Makro expandiert
  • Für GCC und Clang verwende ich ein assert-Makro in der Form while (!(c)) __builtin_unreachable()
  • Diese assert-Variante braucht keine getrennten Build-Konfigurationen
    • Es sind keine separaten Definitionen für Debug- und Release-Builds nötig
    • Ob sie aktiv ist, wird über die Presence des Undefined Behavior Sanitizer, also UBSan, gesteuert
    • libubsan liefert Diagnostik-Ausgaben mit Dateiname und Zeilennummer
    • In Release-Builds ist das ein praktischer Hinweis für Optimierungen
  • Wer Assertions in Release-Builds aktivieren will, setzt UBSan mit -fsanitize-trap in den Trap-Modus und aktiviert mindestens -fsanitize=unreachable
  • Theoretisch ginge auch -funreachable-traps, aber in mehreren aktuellen GCC-Releases war das zum Zeitpunkt des Schreibens kaputt

Was ich in Deklarationen weglasse

  • Bei Parametern verwende ich kein const
    • Ich sehe darin keinen praktischen Nutzen für Optimierungen
    • Mir fällt kein Fall ein, in dem es einen Fehler erkannt hat oder erkannt hätte
    • Gute Parameternamen reichen meiner Ansicht nach als Dokumentation im Prototyp
  • Das Weglassen von const war eine Änderung, die kognitive Last und visuelles Rauschen reduziert und so die Produktivität erhöht
  • Eine kleine Ausnahme bleibt: Als Hinweis, statische Tabellen in Read-Only-Memory in Code-Nähe abzulegen, bevorzuge ich const weiterhin
    • Falls nötig, entferne ich const per Cast
  • Für Null-Pointer verwende ich das Literal 0
    • Das ist seit etwa sieben Jahren mein Stil
    • Theoretische Mängel mag es geben, aber in Hunderttausenden Zeilen Code habe ich keinen realen Fall gesehen
  • restrict verwende ich nur bei Bedarf
    • Ich strukturiere Code so, dass Out-Parameter nicht in Schleifen verwendet werden, oder vermeide Out-Parameter ganz
  • inline verwende ich nicht
    • weil ich alles als eine einzige Translation Unit kompiliere
  • Alle Structs bekommen ein typedef
    • Ohne das Schlüsselwort struct liest sich der Code leichter
    • Rekursive Structs erhalten direkt darüber eine Forward Declaration, und für Felder verwende ich kurze Namen
  • Alle Funktionen außer dem Entry Point deklariere ich als static
    • weil ich von Single-Translation-Unit-Kompilierung ausgehe
  • Durch kurze Typnamen sowie das Weglassen von const und struct kann ich Rückgabetyp und Funktionsnamen bequem in dieselbe Zeile setzen
  • Es gab eine Zeit, in der ich Typnamen großgeschrieben habe, aber das habe ich schließlich wieder aufgegeben

Strings als s8 statt nullterminiert

  • Eine meiner produktivsten Änderungen war, nullterminierte Strings vollständig abzulehnen und stattdessen einen s8-String-Typ mit data und len zu verwenden
  • Aufbau von s8:
    • u8 *data
    • size len
  • Das Makro s8(s) kapselt ein C-String-Literal als s8-String
  • s8 wird wie ein Fat Pointer per Wert übergeben und per Wert zurückgegeben
  • s8 eignet sich auch gut als Funktionspräfix
    • Namen der str-Familie sind reserviert
    • Beispiele: s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • Für Literalvergleiche verwende ich Formen wie s8equals(tagname, s8("body"))
  • Ich habe auch versucht, Größe und Array per flexible array member in einer Allokation zu bündeln, aber der Mangel an Flexibilität wiegt für mich schwerer als die Vorteile
  • Manchmal dachte ich, ein einfaches Programm brauche keinen String-Typ, aber im Rückblick war das meistens ein Fehlurteil
  • Für UTF-16 verwende ich außerdem den Typ s16
    • Er besteht aus c16 *data und size len
    • Ob Literale im Makro mit u präfixiert werden sollten, ist für mich noch nicht ganz entschieden

Struct-Rückgaben und Initialisierung

  • Ich bevorzuge Struct-Rückgaben gegenüber Out-Parametern
    • Im Grunde ist das eine Form, mehrere Werte zurückzugeben, nur ohne Destructuring
  • Im Beispiel i32parse(s8) werden der Parse-Wert value und der Status ok gemeinsam zurückgegeben
  • Zusätzliche Kopierkosten sind in der Praxis aus meiner Sicht kein großes Problem
    • Entweder wandelt die Calling Convention das in einen versteckten restrict-Out-Parameter um
    • oder bei Inlining ist der Overhead des Rückgabewerts bedeutungslos
  • Dieser Stil reduziert die Versuchung, Fehler über besondere In-Band-Signale wie spezielle Null-Rückgaben anzuzeigen
  • Ich bevorzuge das Muster, am Funktionsanfang einen nullinitialisierten Rückgabewert anzulegen und ihn in allen returns zu verwenden
    • Im Fehlerfall wird sofort der nullinitialisierte Zustand zurückgegeben
    • Im Erfolgsfall wird ok erst direkt vor der Rückgabe auf true gesetzt
  • Abgesehen von statischen Daten sowie den Makros s8 und s16 verwende ich Initializer ebenfalls seltener
    • Auch Designated Initializer vermeide ich und initialisiere per Zuweisungen
  • Initialisierung per Zuweisung liest sich gut, und zwischen den einzelnen Zuweisungen liegt jeweils ein Sequence Point, was eine explizite Reihenfolge liefert
  • Bei Initialisierungen wie in Zufallszahlfunktionen, bei denen die Aufrufreihenfolge das Ergebnis beeinflussen kann, muss ich dann nicht mehr über mögliche Auswertungsreihenfolgen nachdenken

Win32-Deklarationen und Inline-Assembler

  • Ich bevorzuge __attribute__ nicht, sondern __attribute
    • Das abschließende __ wirkt auf mich übertrieben und unnötig
  • Bei Win32-Systemprogrammierung binde ich windows.h nicht ein, sondern schreibe die benötigten Prototypen selbst
    • In der Regel ist die Zahl der nötigen Deklarationen und Definitionen nicht groß
    • Das reduziert Build-Zeit und verschmutzt den Namespace weniger
    • Außerdem passt es sauberer zu benutzerdefinierten Typen wie u32, b32 und uptr statt DWORD, BOOL und ULONG_PTR
  • Im Beispiel für Win32-Deklarationen wird das Makro W32(r) __declspec(dllimport) r __stdcall verwendet
    • Funktionen wie ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA und WriteConsoleW werden direkt deklariert
  • In Inline-Assembler behandle ich die äußeren Klammern wie geschweifte Klammern
    • Wie bei if steht vor der öffnenden Klammer ein Leerzeichen
    • Jede Constraint-Zeile beginnt mit einem Doppelpunkt
  • Als Beispiel, in dem man den erwähnten Stil in einem kleinen Programm sehen kann, gibt es wordhist.c
  • Ein etwas größeres Beispiel ist die Implementierung einer Mini-Programmiersprache in asmint.c

1 Kommentare

 
GN⁺ 2023-10-10
Hacker-News-Kommentare
  • Bei #define sizeof(x) (size)sizeof(x) scheint angenommen worden zu sein, dass äußere Klammern nicht nötig sind, aber es gibt eine sehr kleine Ausnahme.
    Casts haben eine höhere Priorität als Multiplikation, daher funktioniert sizeof(x) * 3 sicher als (size)sizeof(x) * 3.
    Bei (size)sizeof(x)[y] wird jedoch Array-Indexing vor dem Cast angewendet, sodass daraus nicht ((size)sizeof(x))[y], sondern (size)(sizeof(x)[y]) wird.
    In realem Code wird man sizeof(x) zwar nicht indexieren, aber C erlaubt integer[pointer] mit derselben Bedeutung wie pointer[integer]; deshalb kann dieses Makro wegen fehlender Klammern kompilieren und dennoch falsch funktionieren.
    Grundsätzlicher fällt es schwer, der Behauptung zuzustimmen, eine signed size sei besser. Der Autor sagt, unsigned sizes seien eine Quelle von Fehlern, aber auch der gezeigte Code hat einen Bug, der den Speicher beschädigt, wenn count negativ ist.
    Bei vorzeichenlosen Ganzzahlen kann eine negative Anzahl nicht dargestellt werden, und selbst bei einem Overflow wird daraus eine sehr große positive Zahl, die von der bestehenden Prüfung erfasst wird. Persönlich bevorzuge ich vorzeichenlose Ganzzahlen, aber möglichst mit einem Wrapper für Bereichsprüfungen, der bei Overflow abbricht.

    • Die Semantik von _Bool gefällt mir im Gegenteil sogar.
      Denn einen Ausdruck, der in if (flags & FLAG_ALLOCATED) gut funktioniert, kann man als boolesche Variable herausziehen, etwa _Bool need_free = flags & FLAG_ALLOCATED;.
      flags & FLAG_ALLOCATED kann, wenn gesetzt, nicht 1, sondern irgendein von 0 verschiedener Wert sein; _Bool normalisiert das auf 1. Wenn man ihn in ein int übernimmt, besteht if (need_free) zwar, aber if (need_free == true) kann fehlschlagen.
      Es gibt auch Nachteile. Wenn man beim Refactoring übersieht, dass die implizite Konvertierung nach _Bool etwas Nützliches getan hat, kann daraus falscher Code wie if ((flags & FLAG_ALLOCATED) == true) werden.
      Außerdem besteht beim Lesen von Strukturen von der Platte oder beim Befüllen mit beliebigen Bytes das Risiko von undefiniertem Verhalten, wenn ein _Bool-Feld weder 0 noch 1 ist.
    • Tatsächlich ist auch (size)(sizeof(x)[y]) für viele überraschend, aber es ist dasselbe wie (size)(sizeof ((x)[y])).
      sizeof ist keine Funktion, sondern ein unärer Operator, und Indexing sowie Funktionsaufrufe haben eine höhere Priorität als sizeof. Deshalb bevorzuge ich es, nach sizeof ein Leerzeichen zu setzen und Klammern um den Operanden nur dann zu verwenden, wenn sie nötig sind.
      https://en.cppreference.com/w/c/language/operator_precedence
      Wenn man das Makro korrekt schreiben will, wird daraus #define sizeof(x) ((size)(sizeof (x))).
    • Gut erkannt. Die Lehre ist: Wenn eine Makrodefinition nicht nur zu einem einzelnen Token expandiert, sollte man sie immer in Klammern einschließen. Die Prioritätsregeln von C sind wirklich kompliziert.
  • Eigene Typen zu definieren wirkt wie ein Schritt zu weit.
    Selbst jemand, der bereits mit C-Typen vertraut ist, muss ein zusätzliches eigenwilliges System lernen, um ein Programm zu verstehen. Größen explizit anzugeben ist sinnvoll, daher ist etwa uint32_t statt uint nachvollziehbar.
    Solche Typen sollten in einem passenden Header definiert sein, und vielleicht liege ich falsch, weil ich C schon lange nicht mehr verwendet habe.

    • In der Praxis ist int in C 32 Bit.
      Nicht auf 16-Bit-Targets, aber wird man ein 5-MB-Programm wirklich auf 16 Bit portieren? Solche Sorgen lohnen sich meist nicht.
      Das Problem ist long. Auf manchen Maschinen sind es 32 Bit, auf anderen 64 Bit, was verwirrend ist. Glücklicherweise ist long long immer 64 Bit, also kann man long einfach weglassen.
      char 8 Bit, short 16 Bit, int 32 Bit, long long 64 Bit, und damit ist es erledigt. In C wurde endlos Zeit mit der Größe von int verschwendet.
    • Der Autor hat es immerhin als persönlichen Coding Style eingegrenzt. Ehrlich gesagt sind die Standardtypen viel zu wortreich, und ich wünschte, die hier vorgeschlagene knappe Liste wäre früher übernommen worden.
    • Etwas scherzhaft gesagt besteht ein erheblicher Teil des Programmierens darin, mit den Typsystemen anderer Leute umzugehen.
      Für Leute, die häufig C schreiben, sind die hier genannten Abkürzungen vertraut, und für ein eigenes Typsystem ist es ziemlich elegant. Es erinnert an Rust.
    • Solche Typen gibt es in stdint.h.
      Es erstaunt mich immer wieder, wie viele Projekte diese Datei mühsam neu erstellen.
      Standardtypen unter eigenen Namen neu zu übersetzen, ist für Leser lästig. Ich habe einmal in einem C++-Projekt gefragt, warum für Collections, Referenzen und zusammengesetzte Objekte massenhaft typedefs verwendet wurden, und bekam die Antwort, das mache es verständlicher.
      Später sah ich neben dem Monitor dieser Person einen typedef-Spickzettel hängen.
    • Eigene Integer-Typdefinitionen sind auf ressourcenbeschränkten Plattformen sinnvoll.
      Man sieht häufig Typen wie dim_t, die je nach Verwendungszweck 32 oder 64 Bit groß sind. Selbst auf 64-Bit-Plattformen werden in Strukturen mit Pointer-Kompression oft 32-Bit-Integer verwendet.
      Wenn man zum Beispiel einen eigenen Heap allokiert und nur 32-Bit-Offsets speichert, halbiert sich bei Workloads unter 4 GB der Speicherverbrauch, und die Cache-Lokalität verbessert sich, was die Performance steigert.
  • Es wirkt etwas übertrieben, etablierte C-Konventionen wegen persönlicher Vorlieben über Bord zu werfen.
    Wenn man statt uint8_t oder int32_t u8 bzw. i32 schreibt, spart man ein paar Zeichen, kann aber andere beim Lesen des Codes verwirren.
    Statt nullterminierter Strings einen eigenen String-Typ zu verwenden, erhöht ebenfalls gefühlt die Schwierigkeit der Zusammenarbeit, wenn man bedenkt, dass C um solche Strings herum entstanden ist.
    Win32-API-Prototypen direkt zu schreiben, ohne windows.h einzubinden, kann zwar die Compile-Zeit verkürzen, fühlt sich aber so an, als würde man den gut ausgebauten Highway stehen lassen und durch den Wald laufen. Vieles davon wirkt eher wie persönlicher Geschmack als wie C-Code, mit dem alle gut umgehen können.

    • Bei u8 oder i32 geht es nicht darum, Tastenanschläge zu sparen, sondern die wahrgenommene kognitive Last beim Lesen zu reduzieren.
      Das in Debatten über Ausführlichkeit versus Knappheit ständig vorgebrachte Argument der „Anzahl der Tastenanschläge“ hat große Schwächen. Die Vorstellung, Knappheit sei nur fürs schnellere Tippen gut und Ausführlichkeit sei beim Lesen immer besser, ist falsch.
      Ausführlichkeit hat beim Verstehen Vorteile, aber Knappheit hat ebenfalls Vorteile, und keine Seite ist eindeutig überlegen. Es sind nur unterschiedliche Kompromisse.
    • Namen wie u16 sind weit verbreitet und dürften Programmierer kaum verwirren.
      Der wirkliche Bruchpunkt ist, wenn zwei verschiedene Programme jeweils u16 definieren und in Header-Dateien veröffentlichen und ein drittes Programm dann beide Header gleichzeitig einbindet.
      Bibliothekstypen mit Namespace sehen dann aus wie libname_u32, und an dem Punkt möchte man statt des Präfixes libname_ lieber einfach uint32_t verwenden.
    • Dass ein fähiger C-Programmierer durch u8 oder i32 verwirrt wird, ist höchstens theoretisch und wirkt etwas wie ein Strohmann-Argument.
      Er mag sich darüber ärgern, aber verwirrt wird er nicht sein. Wie Rich Hickey sagte: Bevor man gelernt hat, etwas zu lesen, ist alles schwer zu lesen.
  • Es heißt, 32-Bit-Booleans zu verwenden könne Anfängern wie Speicherverschwendung erscheinen; dann bin ich wohl auch Anfänger.
    Ich habe ein paar Fälle gehört, in denen sie nicht schlechter sind als 8-Bit-bool, aber keinen, in dem sie tatsächlich besser wären. Wenn in einer Struktur benachbarte Booleans stehen oder eine Boolean-Variable einer Funktion aus den Registern auf den Stack verdrängt wird, verschwendet man weiterhin Speicher.
    Auch wenn es nur ein paar Bytes sind: Warum sollte man absichtlich pessimieren? Was gewinnt man durch die größere Größe?

    • Das hängt vollständig von Architektur und CPU ab, aber aus früherer Erfahrung war ein klarer Fall numerische Verarbeitung.
      Vor den Samples pro Zyklus stand in einer Struktur ein Bedingungswert, danach folgten 512, 1024 oder 2048 Sample-Werte. Ein Junior packte die Struktur, um Platz zu sparen, und machte den Bedingungswert zu einem 8-Bit-Wert mit 1 Byte.
      Dieser „verbesserte“ Code senkte auf Intel-Chips den Durchsatz um etwa den Faktor 10 und löste auf der SPARC-RISC-Architektur einen BUS ERROR aus.
      Durch das Packen des Struktur-Headers war das Datenarray nicht mehr ausgerichtet; Intel musste stillschweigend zwei 32-Bit-Wörter laden und zusammensetzen, während SPARC sich über nicht ausgerichtete Daten zu Recht beschwerte.
      Wenn es nicht um langfristige Dateispeicherung geht, sondern um Pipeline-Berechnungen, bei denen Durchsatz zählt, ist es manchmal besser, Daten an der Architektur-Ausrichtung auszurichten, statt sie auf „Platz sparen“ zu packen.
    • Meist besteht die einfache Optimierung darin, Strukturfelder auf 32-Bit-Grenzen zu padden.
      Fast alle Compiler erledigen das, daher kann man nach „Struktur-Ausrichtung/Padding“ suchen. Wenn der Compiler ohnehin leeren Platz lässt, ist es besser, diesen Speicher selbst zu nutzen; andernfalls kann man Performance verlieren.
      Genauer gesagt sollte jedes Feld an einer Adresse liegen, die durch seine eigene Größe oder die Wortzeilengröße teilbar ist, und die gesamte Struktur sollte ebenfalls auf ein Vielfaches der Größe des größten Felds gepaddet werden. In der Praxis bedeutet das meist 32-Bit-Ausrichtung.
      Siehe: http://www.catb.org/esr/structure-packing/
    • Wenn man den tatsächlichen Typ bool verwendet, warnt ein Sanitizer, wenn der Wert weder false als 0 noch true als 1 ist.
    • Computerarchitekturen sind größtenteils für ausgerichtete Zugriffe ab 32 Bit optimiert. Was man gewinnt, ist normalerweise, aber nicht immer, Performance.
    • Ich frage mich, wie ein Beispiel für eine Funktion aussieht, bei der eine Boolean-Variable auf den Stack verdrängt wird und diese 3 Bytes wichtig werden.
  • Der Aussage zu Struktur-Rückgaben und Ausgabeparametern stimme ich nicht zu.
    Sie macht es viel schwieriger, Funktionen zu kombinieren, die Fehler zurückgeben können, und überall vermehren sich die Typen. In der Praxis kann fast jede Funktion fehlschlagen; besonders wenn man auch Speichermangel behandelt, ist ein vorhersehbarer Stil für Fehler-Rückgaben wichtiger.

    • Speichermangel behandelt fast niemand.
      Das ist sehr schwierig und bringt kaum Nutzen. An diesem Punkt treten Probleme auf, die weit über die Wahl des Programmierstils hinausgehen.
    • Wenn sinnvolles Entpacken von Strukturen möglich wäre, könnte man die übliche errno- und Ausgabeparameter-Semantik bekommen, aber C hat das nicht.
      Trotzdem war das Kombinieren optionaler Werte in C schon immer etwas schmerzhaft. Wenn man keine Exceptions verwendet, wirken je nach Sprache Exceptions und Monaden wie die zwei großen Optionen, aber beides passt nicht zu C und auch nicht zur Philosophie der meisten C-Programmierer.
      Für einfache Eins-zu-eins-Aufrufe kann man es mit Makros versuchen, aber das hat Grenzen. So schrecklich C++ auch ist: C++ optional ist angenehmer zu verwenden als if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }.
    • Optionen oder Summentypen zurückzugeben wäre richtig, ist in C aber wirklich umständlich zu schreiben.
      Das Muster, an jeden Funktionsaufruf if (thing(...)) goto fail anzuhängen, wirkt auch nicht besonders großartig, aber die Go-Seite scheint es zu mögen.
      Oder es gibt thread_local mylibrary_errno; innerhalb einer Bibliothek kann das tatsächlich der passende Weg sein, und an den Grenzen wandelt man es dann in einen Enum-Rückgabewert um.
  • Bei „signed sizes are the way“ wollte ich eigentlich sagen, dass man genau dort aufhören kann zu lesen.
    signed size ist eine ziemlich erstaunliche Leaky Abstraction und eine Methode, die nach Katastrophen ruft.
    Auch die Aussage, const habe keine praktische Rolle und habe noch nie Fehler aufgefangen, ist schwer nachzuvollziehen. Leute verwechseln häufig Eingabe- und Ausgabepuffer, und const macht das sofort sichtbar.
    Auch die Empfehlung, alle Funktionen außer Einstiegspunkten als static zu markieren, kann dazu führen, dass man beim Debuggen Variablen oder Funktionen nicht findet und den Autor verflucht.
    Structs bevorzugt zurückzugeben macht es leicht, versehentlich einen Stack-Pointer zurückzugeben und damit eine große Sicherheitslücke aufzureißen. Übergibt man einen Ausgabepuffer, wird die Ownership-Semantik klar.
    Dieser Rat mag für Leute, die hauptsächlich 64-Bit-Systemcode schreiben, einigermaßen passen, kann aber im 32-Bit-Embedded-Bereich schnell problematisch werden.

    • Bjarne Stroustrup hat ein ausführliches Memo zur Verteidigung von signed size geschrieben.
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • Leute, die const nicht mögen, wird man nie zufriedenstellen; also verwendet man const dort, wo es nötig ist, propagiert es so weit wie nötig und ignoriert das Gemecker.
      Wenn sie es entfernen, fügt man es wieder hinzu. Sie sind immer die, die zuerst müde werden. Ich mache das seit 25 Jahren so und bin immer noch da.
      static kann je nach Tooling unterschiedlich sein. Vor etwa 15 Jahren bin ich zu standardmäßigem static und durchgängigem Einsatz von size_t übergegangen und hatte damit bisher keine Probleme.
  • Interessant ist, dass meine Erfahrung in eine andere Richtung geführt hat.
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    Der Artikel ist zwar auf D ausgerichtet, aber die Prinzipien gelten auch für C.

    • Ich habe ihn mit Interesse gelesen.
      Spannend fand ich den Teil, in dem die Bedingungsausdrücke in doX() und doZ() verschoben werden. Ob das immer richtig ist, weiß ich nicht; es hängt davon ab, wo die Abstraktion liegt und welches mentale Modell man vom Code hat.
      Zum Beispiel ist deleteRecords(); nicht besser als if let x = deadRecords() deleteRecords(x);. Letzteres sieht unordentlicher aus, hat aber den Wert, vorne sichtbar zu machen, dass es kein Löschen, sondern ein Pruning ist.
      Wenn man die Funktion sinnvoll in etwa pruneDeadProjects() umbenennt, ist das in Ordnung; aber nur die Bedingung in die Funktion zu verschieben, kann den Kontext gefährlich machen und zu einer leaky Abstraction werden.
  • Für alle Structs typedef zu verwenden, hilft der Knappheit, daher bin ich dafür.
    Meiner Meinung nach kann man typedef großzügig einsetzen. Allerdings sollte man nur den Zieltyp selbst per typedef benennen, nicht den Pointer. Wenn ein Pointer nötig ist, kann man jederzeit (type *) schreiben.
    Insbesondere bei Funktionspointern sollte man nicht den Funktionspointer, sondern die Funktion selbst typedefen. Dann kann man dieses typedef auch in Funktionsdeklarationen verwenden, bekommt Typprüfung für Parameter und muss bei Änderungen der Funktionssignatur nicht alle Deklarationen anfassen.
    Die meisten C-Codebasen machen das falsch, indem sie Funktionspointer typedefen, und müssen weiterhin Funktionsdeklarationen, die zu dieser Pointer-Definition passen, von Hand schreiben.
    Von Structs als Rückgabetyp bin ich noch nicht überzeugt. Ich bevorzuge es, numerische Fehlercodes als Rückgabewert zu verwenden und die übrigen Rückgabewerte über Ausgabeparameter entgegenzunehmen.

    • Bei opaken Structs verwende ich lieber typedef, um Klassen nachzuahmen, deren Felder alle privat sind, und bei einfachen Datenstrukturen lieber struct.
      Auf Klassen sollte man nur über Funktionen zugreifen, auf Structs sollte direkter Zugriff möglich sein.
      Das entspricht weitgehend den Konventionen des C/POSIX-Standards. Ein Beispiel ist der Unterschied zwischen pthread_t und struct stat.
    • Ich stimme zu, dass man den Pointer selbst nicht per typedef benennen sollte.
      Dinge wie SDL_net machen genau das, und das mag ich nicht. Tatsächlich ist es ein Pointer, aber es wird per typedef wie ein Werttyp behandelt.
      Ich verstehe die Absicht, aber es ist eine ziemlich unangenehme Vorgehensweise.
  • Vieles in diesem Artikel leuchtet mir ein.
    Ich habe angefangen, ein Bare-Metal-OS für Arm64 zu schreiben, und obwohl es noch in einem frühen Stadium ist, mache ich Ähnliches. Ich verwende Pascal-Strings und habe auch die Typnamen geändert. Allerdings im Stil int8, nicht i8.
    Da ich schnell entschieden habe, keine echte Software portieren zu wollen, muss ich mich nicht an Standard-C-Bibliotheksfunktionen oder Konventionen halten. Dadurch kann ich freier experimentieren.
    C ist eine so alte Sprache, dass die Last aus der Zeit, als jedes Byte kostbar war, sogar noch in den Funktionsnamen steckt. Sich davon zu lösen, fühlt sich gut an, und der Inhalt dieses Artikels sowie die vielen kleinen Namensänderungen wirken wie eine ziemlich saubere Aufräumaktion.

    • Dass du keine Standard-C-Bibliotheksfunktionen oder Konventionen befolgen musst, heißt das, dass es nur ein Hobbyprojekt ist und nichts Großes und Professionelles wie GNU werden soll?
    • Dass man sogar bei Symbolen Bytes gespart hat, hatte ich noch nie bedacht.
  • typedef float f32; und typedef double f64; wirken wie eine riskante Grundlage, die voraussetzt, dass float 32 Bit und double 64 Bit groß ist
    OpenCV definiert float16_t, CUDA implementiert Gleitkommazahlen mit halber Genauigkeit, und Mikrocontroller können jeweils eigene Implementierungen haben
    C++23 führt Gleitkommatypen mit fester Breite ein, aber ich weiß nicht, wie man das in C erzwingen könnte. Es scheint besser, ein Makro zu haben, das zur Compile-Zeit prüft, ob kein Datenverlust auftritt
    Insgesamt könnte es, wie andere schon sagten, besser sein, aus Gründen der Lesbarkeit manches beim Standard zu belassen, auch wenn es weniger knapp ist
    [0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
    [1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
    [2] https://en.cppreference.com/w/cpp/types/floating-point