- 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,sizeunds8sowie das Weglassen vonconstundstructsind eine bewusste Entscheidung, um visuelles Rauschen und kognitive Last bei wiederkehrenden Deklarationen zu reduzieren - Strings behandle ich nicht mehr als nullterminierte Zeichenketten, sondern als
s8Fat Pointer mitdataundlen; für Win32- und UTF-16-Umgebungen nutze ich zusätzlichc16unds16 - Beim Funktionsdesign bevorzuge ich Struct-Rückgaben gegenüber Out-Parametern und verwende ein Muster, bei dem ein nullinitialisierter Rückgabewert nur im Erfolgsfall
okgesetzt 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
- Beispiele:
- Diese Namen kommen im ganzen Programm häufig vor, daher hat Kürze direkte Vorteile beim Lesen und Reviewen
- Das Suffix
_tverwende ich nicht mehr; inzwischen wirkt es auf mich visuell unruhig - Für signed-Typpräfixe bevorzuge ich
igegenübersshalte ich für String-Typnamen frei
- Für Größentypen verwende ich
sizestattisize- weil ich signed size als wichtigeren Standard ansehe
usizenutze ich hauptsächlich in dem engeren Fall der Interaktion mit externen Interfaces
b32macht die Absicht „32-Bit-Boolean“ deutlich- Statt
_Boolverwende 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
- Statt
c16ist der Typ für UTF-16-Zeichen, den ich unter Win32 brauche- Auf Basis von
char16_thilft das Debuggern wie GDB, die Daten als Zeichen darzustellen - Der offizielle Win32-Typname ist
wchar_t, aber ich bevorzuge eine explizite UTF-16-Bezeichnung
- Auf Basis von
u8verwende ich für Oktette und meist für UTF-8-Daten,bytedagegen 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_tempfinde ich als unnötige Verschwendung
- Auch lange Typnamen wie
- Wenn ich nur Codeausschnitte separat zeige, verwende ich diese Aliase nicht isoliert
- Denn zum Verständnis brauchen Leser auch die zugehörigen
typedefs
- Denn zum Verständnis brauchen Leser auch die zugehörigen
Regeln für Makros und assert
- Funktionsartige Makros schreibe ich klein
- Beispiele:
countof(a),lengthof(s),new(a, t, n)
- Beispiele:
- 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 Feldnewhaben - Denn nur in Aufrufform wird es als Makro expandiert
- Man kann ein Makro
- Für GCC und Clang verwende ich ein
assert-Makro in der Formwhile (!(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
libubsanliefert 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-trapin 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
constwar 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
constweiterhin- Falls nötig, entferne ich
constper Cast
- Falls nötig, entferne ich
- 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
restrictverwende ich nur bei Bedarf- Ich strukturiere Code so, dass Out-Parameter nicht in Schleifen verwendet werden, oder vermeide Out-Parameter ganz
inlineverwende ich nicht- weil ich alles als eine einzige Translation Unit kompiliere
- Alle Structs bekommen ein
typedef- Ohne das Schlüsselwort
structliest sich der Code leichter - Rekursive Structs erhalten direkt darüber eine Forward Declaration, und für Felder verwende ich kurze Namen
- Ohne das Schlüsselwort
- 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
constundstructkann 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 mitdataundlenzu verwenden - Aufbau von
s8:u8 *datasize len
- Das Makro
s8(s)kapselt ein C-String-Literal alss8-String s8wird wie ein Fat Pointer per Wert übergeben und per Wert zurückgegebens8eignet sich auch gut als Funktionspräfix- Namen der
str-Familie sind reserviert - Beispiele:
s8span,s8equals,s8compare,s8hash,s8trim,s8clone
- Namen der
- 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 *dataundsize len - Ob Literale im Makro mit
upräfixiert werden sollten, ist für mich noch nicht ganz entschieden
- Er besteht aus
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-Wertvalueund der Statusokgemeinsam 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
- Entweder wandelt die Calling Convention das in einen versteckten
- 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
okerst direkt vor der Rückgabe auf true gesetzt
- Abgesehen von statischen Daten sowie den Makros
s8unds16verwende 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
- Das abschließende
- Bei Win32-Systemprogrammierung binde ich
windows.hnicht 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,b32unduptrstattDWORD,BOOLundULONG_PTR
- Im Beispiel für Win32-Deklarationen wird das Makro
W32(r) __declspec(dllimport) r __stdcallverwendet- Funktionen wie
ExitProcess,GetStdHandle,VirtualAlloc,WriteConsoleAundWriteConsoleWwerden direkt deklariert
- Funktionen wie
- In Inline-Assembler behandle ich die äußeren Klammern wie geschweifte Klammern
- Wie bei
ifsteht vor der öffnenden Klammer ein Leerzeichen - Jede Constraint-Zeile beginnt mit einem Doppelpunkt
- Wie bei
- 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
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) * 3sicher 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 erlaubtinteger[pointer]mit derselben Bedeutung wiepointer[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
countnegativ 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.
_Boolgefä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_ALLOCATEDkann, wenn gesetzt, nicht 1, sondern irgendein von 0 verschiedener Wert sein;_Boolnormalisiert das auf 1. Wenn man ihn in einintübernimmt, bestehtif (need_free)zwar, aberif (need_free == true)kann fehlschlagen.Es gibt auch Nachteile. Wenn man beim Refactoring übersieht, dass die implizite Konvertierung nach
_Booletwas Nützliches getan hat, kann daraus falscher Code wieif ((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.(size)(sizeof(x)[y])für viele überraschend, aber es ist dasselbe wie(size)(sizeof ((x)[y])).sizeofist keine Funktion, sondern ein unärer Operator, und Indexing sowie Funktionsaufrufe haben eine höhere Priorität alssizeof. Deshalb bevorzuge ich es, nachsizeofein 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))).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_tstattuintnachvollziehbar.Solche Typen sollten in einem passenden Header definiert sein, und vielleicht liege ich falsch, weil ich C schon lange nicht mehr verwendet habe.
intin 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 istlong longimmer 64 Bit, also kann manlongeinfach weglassen.char8 Bit,short16 Bit,int32 Bit,long long64 Bit, und damit ist es erledigt. In C wurde endlos Zeit mit der Größe vonintverschwendet.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.
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.
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_toderint32_tu8bzw.i32schreibt, 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.heinzubinden, 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.u8oderi32geht 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.
u16sind weit verbreitet und dürften Programmierer kaum verwirren.Der wirkliche Bruchpunkt ist, wenn zwei verschiedene Programme jeweils
u16definieren 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äfixeslibname_lieber einfachuint32_tverwenden.u8oderi32verwirrt 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?
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.
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/
boolverwendet, warnt ein Sanitizer, wenn der Wert wederfalseals 0 nochtrueals 1 ist.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.
Das ist sehr schwierig und bringt kaum Nutzen. An diesem Punkt treten Probleme auf, die weit über die Wahl des Programmierstils hinausgehen.
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) { ... }.Das Muster, an jeden Funktionsaufruf
if (thing(...)) goto failanzuhä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,
consthabe keine praktische Rolle und habe noch nie Fehler aufgefangen, ist schwer nachzuvollziehen. Leute verwechseln häufig Eingabe- und Ausgabepuffer, undconstmacht das sofort sichtbar.Auch die Empfehlung, alle Funktionen außer Einstiegspunkten als
staticzu 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.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
constnicht mögen, wird man nie zufriedenstellen; also verwendet manconstdort, 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.
statickann je nach Tooling unterschiedlich sein. Vor etwa 15 Jahren bin ich zu standardmäßigemstaticund durchgängigem Einsatz vonsize_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.
Spannend fand ich den Teil, in dem die Bedingungsausdrücke in
doX()unddoZ()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 alsif 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
typedefzu verwenden, hilft der Knappheit, daher bin ich dafür.Meiner Meinung nach kann man
typedefgroßzügig einsetzen. Allerdings sollte man nur den Zieltyp selbst pertypedefbenennen, 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.
typedef, um Klassen nachzuahmen, deren Felder alle privat sind, und bei einfachen Datenstrukturen lieberstruct.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_tundstruct stat.typedefbenennen sollte.Dinge wie SDL_net machen genau das, und das mag ich nicht. Tatsächlich ist es ein Pointer, aber es wird per
typedefwie 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, nichti8.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.
typedef float f32;undtypedef double f64;wirken wie eine riskante Grundlage, die voraussetzt, dassfloat32 Bit unddouble64 Bit groß istOpenCV definiert
float16_t, CUDA implementiert Gleitkommazahlen mit halber Genauigkeit, und Mikrocontroller können jeweils eigene Implementierungen habenC++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
_Float-Typentypedef _Float32 f32;typedef _Float64 f64;https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.html