C11: Neue Version des Sprachstandards, Teil 2

Seite 3: Unicode, Math

Inhaltsverzeichnis

Wo Programmiersprachen der Neuzeit wie Java seit jeher eine gute Unicode-Unterstützung mitbringen, muss C dieses Feature erst nachrüsten. Um der Internationalisierung Rechnung zu tragen, erweitert nun C11 den Unicode-/ISO-10646-Support. Das betrifft neben Datentypen und Präfixen auch einige andere Funktionen. C dient aber nur als Grundgerüst und liefert lediglich die notwendigen Funktionen, damit Nutzer und Bibliotheken ein tragfähiges Grundgerüst vorfinden. Konvertierungsfunktionen, um zwischen den Kodierungen zu konvertieren, werden beispielsweise nicht angeboten.

Der Unicode-Standard unterstützt drei Kodierungsformen: UTF-8, UTF-16 und UTF-32. Der aktuelle Unicode-Standard 6.0 benötigt 21 Bit, um den definierten Bereich abzudecken. Das große 'A' wird dabei mit U+0041 beschrieben, 8 Bit sind ausreichend, also UTF-8. Das japanische Katakana ist definiert als Unicode-Codepoint U+30A1. Mit UTF lässt sich das mit 8 Bit nicht kodieren. Unicode legt nicht fest, in welcher Kodierung ein Zeichen zu kodieren ist. Beide Zeichen lassen sich als UTF-8 (dann aber mit vier Byte), UTF-16 oder UTF-32 kodieren.

wchar_t ist seit C95 definiert und beschreibt ein Multibyte-Zeichen. Es hat aber keine definierte Breite, auch ein nur 8 Bit breiter wchar_t wäre standardkonform. Unter Windows ist wchar_t 2 Byte, unter Linux 4 Byte breit, und der Text wird als Unicode-32 kodiert. Ob "little" oder "big endian", hängt von der aktuellen Architektur ab. wchar_t ist somit überhaupt nicht über Plattformgrenzen kompatibel. Aus den Gründen ist in der aktuellen Unicode-Spezifikation unter Kapitel 5 ein Passus enthalten, der davor warnt, wchar_t zu verwenden, wenn mit Unicode kodierter Text gespeichert werden soll. wchar_t lässt sich also als plattformspezifischer Typ betrachten, um Wide-Zeichen zu speichern, die dann in Unicode kodiert sein können.

Um dieses Dilemma aufzulösen, führt C11 zwei neue Datentypen ein: char16_t und char32_t. Die Datentypen sind geeignet, um mit UTF-16 und UTF-32 kodierte Zeichensequenzen aufzunehmen. Die Deklaration für die Typen ist in der neuen Header-Datei uchar.h zu finden. UTF-8 ist ein 8-Bit-Code und lässt sich deshalb normal in einem Zeichentyp speichern. Ein char erfüllt seit jeher die Anforderung, mindestens 256 verschiedene Zustände aufzunehmen. char8_t gibt es deshalb nicht.

Um einen Unicode-String zu definieren, ist ein String-Literal zu verwenden. Neu hinzugekommen sind u8, u und U. Beispielsweise erzeugt u"Hello" einen UTF-16-kodierten, u8"Hello" einen UTF-8-kodierten und U"Hello" einen UTF-32-kodierten String.

Es wäre falsch, davon auszugehen, dass char16_t und [/i]char32_t[/i] einen Unicode-Zeichensatz kodieren, grundsätzlich lässt sich darin jede andere mögliche Kodierung speichern. In uchar.h ist __STDC_UTF_16__ definiert, falls Werte des Typs char16_t in UTF-16 kodiert sind. Es steht dann dem Kompiler frei, andere Makros zu definieren, falls eine andere Kodierung verwendet wird. Das ist aber theoretischer Natur.

Daneben definiert die Standardbibliothek vier neue Funktionen, die die Größe des benötigten Speicherbereichs liefert. Das wird verwendet, falls man die exakte Länge eines Puffers benötigt: c16rtomb() mbrtov16() c32rtomb() mbrtoc32().

Bereits seit C99 unterstützt C komplexe Zahlen. Der reale und imaginäre Anteil lässt sich dabei mit einfacher (complex float), doppelter (complex double) oder erweiterter doppelter (complex long double) Gleitpunktgenauigkeit speichern. Eine komplexe Zahl mit einfacher Genauigkeit kann der Programmierer wie folgt initialisieren:

float complex a = 4.2 + 2.3I;

Wobei complex ein Makro für _Complex und I eines für _Complex_I ist, zumindest unter GCC. Kommt I als Identifier zum Einsatz, lässt sich _Complex_I auch direkt verwenden. Lediglich I ist nach der Inkludierung von complex.h mit #undef I zu entschärfen.

Die vier Grundrechenarten werden normal abgedeckt: Addition, Subtraktion, Multiplikation und Division zweier komplexer Zahlen sind ohne Verrenkung durch die arithmetischen Operatoren +, -, *, / möglich. Die Standardbibliothek stellt darüber hinaus eine Liste weiterer Funktionen und Makros zur Verfügung, beispielsweise für die trigonometrische Berechnungen oder wenn der reine Realanteil einer komplexen Zahl benötigt wird (crealf()).

All das hat bereits C99 eingeführt. C11 beseitigt nun ein Manko bei der Initialisierung. C99 besagt, dass sich ein komplexer Datentyp mit folgendem Ausdruck erzeugen lässt: x + y*I. Dabei sind x und y reale Datentypen, und es ergibt sich ein nicht triviales Problem, das sich für die Standardbibliothek nicht so einfach umgehen lässt: Ist I als komplexer Typ definiert und y keine natürliche Zahl, sondern NaN (Not a Number, ein an sich gültiger "Wert"), resultiert der Ausdruck x+y*I zu NaN+NaN*<i> statt des korrekten x + NaN*<i>. Um das zu lösen, führt C11 drei neue Funktionen ein, die bei der Initialisierung komplexer Zahlen helfen:

double complex cmplx( double x, double y); 
float complex cmplxf( float x, float y);
long double complex cmplxl( long double x, long double y);

Dabei erzeugen alle drei Funktionen ein komplexes Datum, ausgehend von zwei realen Werten nach dem bekannten Schema x + y*i.

C11 definiert einen weiteren Weg, um ein Programmbzu beenden. Bis dato ließ sich mit exit() ein Programm normal beenden. Dabei werden alle Funktionen, die vorab via atexit() registriert wurden, in umgekehrter Reihenfolge aufgerufen. Alle offenen Streams (z. B. fopen()) mit gepufferten Daten werden "geflushed" und geschlossen sowie alle temporären Dateien gelöscht. Letztlich wird der Parameter von exit an die Host-Umgebung weitergereicht und signalisiert damit einen erfolgreichen oder erfolglosen Status.

Daneben existiert _Exit(), das keine der vorab registrierten Exit-Funktionen aufruft. Ob gepufferte Daten "geflushed" oder offene Streams geschlossen werden, ist implementierungsabhängig und wird vom Standard nicht explizit definiert. In Unix-Umgebungen kommt _exit zum Einsatz, wenn zwischen Prozessen Ressourcen wie Socket-Deskriptoren gemeinsam verwendet werden. Beendet sich ein Kindprozess, soll beispielsweise nicht unbedingt der Server-Socket im Elternprozess geschlossen werden.

abort() läutet ein unregelmäßiges Ende ein. Ob offene Streams geschlossen oder ob temporäre Dateien gelöscht werden, ist auch hier implementierungsabhängig. Unter Unix-Betriebssystemen mit ungesetzten Limits für Speicherauszügen (ulimit -c unlimited) wird darüber hinaus ein Speicherabzug erstellt.

C11 definiert nun zwei weitere Funktionen, die einen weiteren Weg eröffnen, um ein Programm zu beenden: at_quick_exit(), um Funktionen zu registrieren, die beim Beenden ausgeführt werden, und quick_exit(), um das Programm zu schließen. Der Hintergrund für diese Funktion ist in der Interoperabilität mit C++ in Thread-Umgebungen zu suchen. Der entscheidende Unterschied zu den vorab genannten Funktionen ist, dass quick_exit() keine Destruktoren von Objekten mit statischen oder Thread-lokalen Storage aufruft. Nun könnte man auf die Idee kommen, dass _exit() das nicht ausführt. _exit() leistet aber faktisch keine Aufräumarbeiten – und das will man eventuell auch nicht. Zum Beispiel möchte man wenigstens noch kritische Daten in eine Datei schreiben, bevor es sich beendet. Genau die Zwischenmöglichkeit eröffnet quick_exit(): Mit atexit() registrierte Funktionen werden nicht aufgerufen, via qt_quick_exit() registrierte aber wohl.