1 Punkte von GN⁺ 2024-10-19 | 1 Kommentare | Auf WhatsApp teilen
  • P3477R0 ist ein Vorschlag zur Änderung des Standards, der die Byte-Größe in C++ nicht länger dem implementierungsdefinierten Wert CHAR_BIT überlassen, sondern genau auf 8 Bit festschreiben will
  • Moderne Plattformen haben sich bereits auf die Annahme von 8-Bit-Bytes eingependelt, und auch GCC, LLVM und MSVC setzen entsprechende Standardwerte oder Makros auf 8
  • POSIX verlangt seit POSIX.1-2001 CHAR_BIT == 8, und auch die Einführung der Zweierkomplement-Darstellung für Ganzzahlen in C++20 und C23 weist in dieselbe Richtung
  • Die Unterstützung von Bytes mit nicht 8 Bit hinterlässt kleine Ausnahmen in Sprache, Bibliotheken und Toolchains und erzeugt damit die Last eines Edge Case, der nicht zur realen modernen C++-Nutzung passt
  • Ausnahmen wie PDP-10 oder bestimmte DSP-Architekturen existieren zwar, doch die Kernfrage ist, ob neue C++-Standards für diese Zielsysteme weiterhin Komplexität mitschleppen sollten

Ziel der Änderungen in P3477R0

  • C++ übernimmt das Makro CHAR_BIT aus C; derzeit ist dieser Wert ein implementierungsdefinierter Wert, der die Anzahl der Bits pro Byte angibt
  • P3477R0 schlägt vor, den C++-Standard so zu ändern, dass ein Byte offiziell 8 Bit haben muss
  • Dahinter steht die Einschätzung, dass die frühere Flexibilität für verschiedene Byte-Größen in den Anfangszeiten des Computings sinnvoll war, moderne Hardware heute aber fast vollständig auf 8-Bit-Bytes konvergiert ist

Aktueller Stand bei Compilern und Plattformen

  • Führende Compiler behandeln 8-Bit-Bytes bereits als praktische Realität
    • GCC verwendet standardmäßig 8, und es gibt kein Upstream-Ziel, das diesen Standardwert ändert
    • LLVM setzt __CHAR_BIT__ auf 8
    • MSVC definiert CHAR_BIT als 8
  • Unter den historischen GCC-Unterstützungsfällen wurde dsp16xx 2004 entfernt, 1750a bereits 2002
  • Bei Websuchen finden sich zwar einige externe GCC-Ports mit BITS_PER_UNIT ungleich 8, diese scheinen jedoch keinen Bezug zu modernem C++ zu haben

Entwicklung bei POSIX und Ganzzahldarstellungen

  • POSIX verlangt seit POSIX.1-2001 folgende Bedingungen
    • Ein Byte hat genau 8 Bit
    • CHAR_BIT ist 8
    • SCHAR_MAX ist 127, SCHAR_MIN ist -128, UCHAR_MAX ist 255
  • POSIX erläutert, dass daraus infolge der Einführung von int8_t ein 8-Bit-char und Zweierkomplement-Arithmetik erforderlich werden
  • C++20 unterstützt seit P0907r4 nur noch Zweierkomplement-Speicherrepräsentationen, und C23 folgt derselben Richtung
  • Als Beispiele aktuell POSIX-konformer Betriebssysteme werden AIX, HP-UX, INTEGRITY, macOS, OpenServer, UnixWare, VxWorks und vz/OS aufgeführt

Welche Kosten Nicht-8-Bit-Bytes verursachen

  • Software für 8-Bit-Bytes und Software für Nicht-8-Bit-Bytes sind nicht miteinander kompatibel; C/C++-Code für Nicht-8-Bit-Byte-Ziele kommt damit praktisch einem inkompatiblen Dialekt von C und C++ gleich
  • Die Unterstützung von Architekturen mit Nicht-8-Bit-Bytes hinterlässt an vielen Stellen in Sprache und Bibliotheken kleine, aber unnötige Komplexität
  • Compiler und Toolchains müssen weiterhin einen Edge Case mittragen, der die moderne Nutzung nicht widerspiegelt
  • Neue Programmierer können durch diese exotische Eigenschaft von C++ leicht verwirrt werden
  • Manche erfahrene Programmierer investieren nach Ansicht des Vorschlags Zeit in „Portabilität“ für Plattformen, die in der Praxis nicht existieren

Ausnahme-Architekturen und Kompromissvorschläge

  • Der Vorschlag erkennt an, dass es weiterhin Prozessoren mit Nicht-8-Bit-Bytes gibt
  • Die Kernfrage lautet, ob solche Prozessoren für modernes C++ noch relevant sind und ob deren Nutzer überhaupt neue C++-Versionen einsetzen würden
  • Als Kompromiss wird auch die Forderung CHAR_BIT % 8 == 0 genannt; sinnvoll wäre das allerdings nur, wenn das Komitee weiterhin DSPs oder andere Prozessoren unterstützen will, bei denen CHAR_BIT zwar nicht 8 ist, aber ein Vielfaches davon
  • PDP-10 wird als Diskussionsfall genannt, während PDP-11 ausdrücklich als System mit 8-Bit-Bytes abgegrenzt wird
  • Manche DSPs behandeln 24-Bit- oder 32-Bit-Wörter wie „Bytes“; solche Architekturen waren in einer Zeit plausibel, in der Wortgrößen stark variierten und das Byte-Konzept noch nicht standardisiert war

Richtung der Änderungen am Standardtext

  • In intro.memory soll die Byte-Definition so geändert werden, dass das Byte als grundlegende Speichereinheit des C++-Speichermodells ausdrücklich 8 Bit umfasst
  • Für climits wird eine Formulierung vorgeschlagen, nach der CHAR_BIT den Wert 8 hat
  • In cstdint wären wegen der 8-Bit-Bytes Integer-Typen mit festgelegter Breite wie int8_t, uint8_t sowie die dazugehörigen Makros nicht länger optional
  • Typen mit _N_, bei denen N nicht 8, 16, 32 oder 64 ist, blieben weiterhin optional
  • Dazu gehört auch die Streichung von vier Mandates-Klauseln in localization, die sich auf CHAR_BIT == 8 beziehen

Verhältnis zum C-Standard

  • Der Vorschlag prüft, ob C++ weiterhin an Architekturen mit Nicht-8-Bit-Bytes festhalten sollte
  • Das C-Komitee könnte für die Sprache C zu einem anderen Ergebnis kommen
  • Eine Abstimmung beider Komitees wäre ideal, doch der Vorschlag sieht hier vor allem vor, dass die Liaison-Gruppe WG14 und SG22 WG21 mit Informationen versorgt

1 Kommentare

 
GN⁺ 2024-10-19
Meinungen auf Hacker News
  • In JFs Reihe „Können wir nicht anerkennen, dass echte Computer alle so funktionieren?“ gab es bereits die Folge vorzeichenbehaftete Ganzzahlen sind Zweierkomplement: „Signed Integers are Two’s Complement

  • Als Praktikant schrieb ich 1986 C-Code auf einer BBN C/70 mit 10-Bit-Bytes; das war eine schreckliche Erfahrung, und schon die Existenz solcher Maschinen war im negativen Sinne ein kosmischer Unfall

    • Ich habe einmal Code auf einem DECSYSTEM-20 geschrieben, aber der C-Compiler war nicht offiziell unterstützt
      Es nutzte 36-Bit-Wörter und 7-Bit-Bytes, und wenn man Bytes in ein Wort packte, blieben Bits übrig. Dann bekam ich auch noch die Aufgabe, Bänder mit Binärdaten im 8-Bit-Format zu lesen – das war ein Chaos
    • Ich habe die CPU der Intel Intellivision programmiert; das war eine merkwürdige Maschine mit 10-Bit-decl und nicht leistungsfähig genug, um C laufen zu lassen
    • Ich habe an Maschinen mit 9-Bit-Bytes und 81-Bit-Instruktionen sowie an Maschinen mit 6-Bit-Bytes gearbeitet, aber für beide gab es keinen C-Compiler
    • In heutigen FPGAs ist 10-Bit-Arithmetik tatsächlich nicht selten und kommt auch in vergleichsweise modernen Produkten vor
      Aber 10-Bit-C ist eine andere Geschichte
  • D machte einen großen Schritt nach vorn, indem es festlegte: Ein Byte hat 8 Bit, short 16 Bit, int 32 Bit, long 64 Bit, Arithmetik ist Zweierkomplement, Fließkomma ist IEEE-Fließkomma
    Das sparte enorm viel Zeit, die man sonst damit verbracht hätte, solche Dinge zu abstrahieren und am Ende doch falsch zu liegen, und Millionen Menschen waren erleichtert. Auch der Zeichensatz war Unicode, nicht EBCDIC oder RADIX-50

    • Zig ist noch besser: Größen sind explizit, etwa u8/i8, u16/i16, u32/i32, u64/i64, und auch die Arithmetik wird ausdrücklich gewählt
      Überlauf bei + ist fehlerhaftes Verhalten und führt in Debug und releasesafe zum Abbruch, +% ist Zweierkomplement-Wrapping, +| ist saturierende Arithmetik. @addWithOverflow() liefert ein Tupel aus dem ursprünglichen Typ und u1, und std.math.add() gibt bei Überlauf einen Fehler zurück. f16, f32, f64, f80, f128 sind jeweils IEEE-Fließkommatypen der entsprechenden Bitlänge. Wie lang ein Byte ist, spielt keine Rolle; auf einer Maschine mit 12-Bit-Bytes würde man eben u12 und i12 verwenden
    • Dass D einen großen Schritt nach vorn gemacht habe, ist übertrieben. Typnamen mit expliziter Größe wie u8, i32 sind in jeder Hinsicht viel besser
    • Wenn „ein Byte 8 Bit hat“, wie groß ist dann ein Bit?
    • Dass Herr Bright, der Autor der Sprache D, das selbst sagt, wirkt ein wenig nach Eigenlob, oder? :)
    • Java hat diesen Teil ebenfalls richtig gemacht. unsigned wurde falsch behandelt, aber die Standardisierung der Bitbreiten primitiver Typen wurde ordentlich gelöst
      byte = 8 bits, short = 16, int = 32, long = 64, float = 32 bit IEEE, double = 64 bit IEEE
  • Es gibt immer noch Leute, die mit DSPs arbeiten müssen: https://thephd.dev/conformance-should-mean-something-fputc-and-freestanding#we-cannot-program-on--vibes-
    Persönlich dokumentiere ich zum Spaß eine nicht implementierte 12-Bit-Fantasy-Konsole mit „50 % mehr Bits pro Byte als die Konkurrenz!“ und habe auch Erfindungen wie „UTF-12“ eingebaut

    • Ich versuche noch herauszufinden, welche Ziele weiterhin relevant sind und ob sie modernes C++ anvisieren oder das planen
      Ich habe jahrelang gefragt, aber keine positive Antwort erhalten; genannt wurde im Grunde nur TI, daher habe ich dem aktualisierten Entwurf Informationen hinzugefügt: https://isocpp.org/files/papers/D3477R1.html
    • Man könnte doch einfach C++23 oder älter als Ziel nehmen. Ich habe ein paar SHARCs, aber wenn das Komitee in einer Version wie C++30 die Unterstützung für CHAR_BIT=32 streicht, werde ich nicht weinend protestieren
    • Hat der PDP-8 nicht 12-Bit-Bytes verwendet?
  • Ich frage mich, ob C++ überhaupt etwas abschaffen oder vereinfachen kann.
    Ehrliche Frage, ich habe das nicht im Detail verfolgt. Ich habe gehört, rand() sei kaputt und nicht reparierbar; nach meinem letzten Stand war es aber noch nicht einmal zur Abschaffung vorgesehen. Dieser Vorschlag wirkt wie ein Test nach dem Motto: „Können wir auch die Unterstützung für Lösungen zu Problemen entfernen, die buchstäblich niemand hat?“

    • Vorzeichenbehaftete Ganzzahlen mussten nicht zwingend im Zweierkomplement dargestellt werden; Vorzeichen-Betrag, Einerkomplement und Zweierkomplement waren alle drei gültig.
      Modernes C und C++ haben das aufgegeben und verlangen Zweierkomplement. Hier ist auch die „as if“-Unterscheidung praktisch nicht relevant, und auf CHAR_BIT lässt sich das auf dieselbe Weise anwenden; es gibt also eindeutig einen Präzedenzfall für solche Änderungen.
    • Trigraphs wurden bereits entfernt, und rand wurde ebenfalls zur Deprecation vorgesehen, inklusive Alternativen.
      Außerdem gibt es p2809 Trivial infinite loops are not Undefined Behavior, p1152 Deprecating volatile, p0907 Signed Integers are Two's Complement, p2723 Zero-initialize objects of automatic storage duration und p2186 Removing Garbage Collection Support. Änderungen sind also möglich.
    • Die GC-API aus C++11 wurde in C++23 entfernt, und das ist nachvollziehbar, weil sie nicht mit Blick auf die Anforderungen der wichtigsten GC-Varianten, Unreal C++ und C++/CLI, entworfen wurde.
      Exception Specifications wurden ebenfalls entfernt, auch wenn manche sie für Value-Type-Exceptions gern zurückhaben möchten. auto_ptr wurde wegen seines kaputten Designs ebenfalls entfernt. Unter dem Aspekt der Vereinfachung bringt das allerdings nicht viel, weil man die alte Art trotzdem noch kennen muss.
    • Das wirkt wie eine Satire darauf, dass man Perfektion nicht brechen, sondern noch mehr Perfektion anhäufen soll.
      Also etwa: Man brauche ein neues C++-Symbol, das zuverlässig auf ein 8-Bit-Byte verweist, ohne Kompatibilität zu brechen. Zum Beispiel könnte man unsigned byte8, ein Zweierkomplement-signed byte8 und ein byte8 mit undefiniertem Vorzeichenverhalten einführen. Für Buchhalter dann noch unsigned decimal byte8 und signed decimal byte8, deren Wertebereich auf 0–10 bzw. -10 bis +10 beschränkt ist; für Buchhalter, die auch Byte-Kosten mitrechnen, ein centimal byte8 mit 0–100 bzw. -100 bis +100; einen ungefähr ausreichenden Typ für das age-Feld in Datenbanken; und natürlich auch float byte8.
    • Ich verstehe nicht, warum rand() kaputt sein soll. Es erzeugt Werte, die zufällig aussehen, und genau das ist sein Zweck.
      Dass es keine kryptografisch sicheren Zufallszahlen erzeugt, ist selbstverständlich, und entsprechende Funktionen in anderen Sprachen tun das auch nicht. Wenn man schnell berechnete, einigermaßen zufällige Ganzzahlen braucht, funktioniert rand() völlig ausreichend.
  • Danke für das Interesse an dem Vorschlag; auf Basis des bisherigen Feedbacks habe ich einen aktualisierten Entwurf erstellt: https://isocpp.org/files/papers/D3477R1.html

    • Mir gefällt der sarkastische Ton des Vorschlags.
      Besonders eindrucksvoll ist der Satz: „Die Frage ist nicht, ob es noch Architekturen gibt, auf denen ein Byte nicht 8 Bit hat. Die gibt es! Die Frage ist, ob ihnen modernes C++ wichtig ist – und ob sie modernem C++ wichtig sind.“
  • Bei diesem Vorschlag bin ich zwiegespalten. Einerseits ist er offensichtlich richtig, und es gibt keinen sinnvollen Anwendungsfall dafür, dass CHAR_BIT nicht 8 ist.
    Andererseits fühlt es sich auch so an, als würde man einer Just-World-Sicht nachgeben, nach der die Welt allein auf Basis eines persönlichen, übermäßig vereinfachten Modells des Computerinneren sinnvoll und nachvollziehbar sein müsse. Mit so einem Ansatz kommt man ziemlich weit, aber letztlich endet er in einer Sackgasse; am Ende muss man akzeptieren, dass man nichts weiß und dass das Beste, was man hat, ein formales Argument ist, dass man nur unter der Bedingung, dass die Dokumentation stimmt, ein korrektes Programm konstruiert hat. Das ist ein großer gedanklicher Sprung, und persönlich fiel es mir umso schwerer, ihn später zu machen, je länger ich nicht gezwungen war, ihn anzuerkennen. Trotzdem scheinen physische Elektronikprojekte unter Einsteigern heute beliebt zu sein; vielleicht wird also statt „Lies die Dokumentation“ künftig „Lies das verdammte Datenblatt“ zum neuen Standard.

    • Trotzdem sehe ich jedes Mal, wenn ich ein autoconf-Skript ausführe, dass die Anzahl der Bits pro Byte geprüft und in config.h gespeichert wird. Als hätte irgendjemand tatsächlich vor, je nach diesem Wert zu handeln.
    • Eine weit verbreitete Sprache stößt am Ende zwangsläufig auf das COBOL-Problem. In den meisten Fällen ist alles in Ordnung, aber in irgendeinem System, das zum Update gezwungen wurde, kann plötzlich eine Verkehrsleitstelle stehen bleiben oder ein Flugzeug abstürzen.
      Man bräuchte eine Möglichkeit, den gesamten bestehenden Code im Zuge der Kompilierung daraufhin zu prüfen, ob dieses Makro bereits verwendet wird. Solche Breaking Changes bergen auch das Risiko, die Sprache zu spalten. Es ist auch nicht klar, wie schwierig es wäre zu testen, ob bestehende Codebases das CHAR_BIT-Makro verwenden und ob sie auf einen neuen Compiler aktualisiert werden können. Fragen wären auch, welche Bibliotheken als kaputt gelten würden und ob es Probleme bei der Interaktion mit anderem Code gibt, der mit Verwendung von CHAR_BIT kompiliert wurde. Ich stimme zu, dass das kontraintuitiv ist, aber besser wäre es, zuerst Konvertierungswerkzeuge zu bauen, zu zeigen, dass es selbst in Extremfällen sicher ist, und dann umzustellen.
  • Mir gefällt der Vorschlag, weil er zugleich unumstritten und unglaublich scharf ist.

  • int8_t == char == 8 bits zu erzwingen, ist für mich völlig in Ordnung, aber ich bin nicht sicher, ob man das Missverständnis verbreiten sollte, ein Byte sei 8 Bit.
    Ein 8-Bit-Byte nennt man Oktett (octet). Gleichzeitig ist byte seit C++17 bereits so etwas wie ein „Alias“ für char: https://en.cppreference.com/w/cpp/types/byte

    • Ich hatte vor 45 Jahren zum ersten Mal mit Computern zu tun, und schon damals war „Byte“ als 8-Bit-Menge definiert.
      In den 45 Jahren danach habe ich nie erlebt, dass „Byte“ etwas anderes bedeutete; wenn es also eine Definition von „Byte“ gibt, die nicht 8 Bit meint, bräuchte ich dafür eine Quelle.
    • Networking-RFCs verwenden von Anfang an konsequent den Ausdruck octet.
    • Nein, ein Byte hat 8 Bit.
      Das ist keine deskriptive Aussage, sondern eine normative Aussage.
    • Persönlich mag ich int8 == signed char nicht.
      std::cout << (int8_t)32 << std::endl; sollte selbstverständlich 32 ausgeben.
  • Hat zwar nichts mit C++ zu tun, aber die Idee eines Retro-Mikrocomputers mit 6-Bit-Bytes gefällt mir ziemlich gut. 24 Bit wären dann ein Wort.
    Mikrocomputer arbeiten meist mit einer kleinen Zahl von Objekten und bevorzugen Arrays gegenüber Pointern, sodass man Speicher sparen könnte. VGA hatte 6 Bit pro Farbe, mit einer 6x4-Bit-Matrix lassen sich lesbare Buchstaben erstellen, grundlegende LISP- oder Forth-Sprachen passen ebenfalls in ein 6-Bit-Alphabet, und das ursprüngliche System/360 nutzte nur 24-Bit-Adressen. 12 MiB Speicher mit unabhängig adressierbaren 6-Bit-Einheiten sollten für jeden genug sein. Falls es nicht reicht, könnte man FAT-12 ganz natürlich zu FAT-24 erweitern oder 48-Bit-Pointer verwenden, die ungefähr so nützlich sind wie 64-Bit-Pointer.

    • Oder man nimmt 8-Bit-Bytes und 3-Byte-Wörter. Das sind immer noch 24 Bit.