Wie gut wird Unicode in C ++ 11 unterstützt?

183

Ich habe gelesen und gehört, dass C ++ 11 Unicode unterstützt. Ein paar Fragen dazu:

  • Wie gut unterstützt die C ++ - Standardbibliothek Unicode?
  • Tut std::stringwas es soll?
  • Wie benutze ich es?
  • Wo liegen mögliche Probleme?
Ralph Tandetzky
quelle
19
"Tut std :: string was es sollte?" Was denkst du sollte es tun?
R. Martinho Fernandes
2
Ich benutze utfcpp.sourceforge.net für meine utf8-Bedürfnisse. Es ist eine einfache Header-Datei, die Iteratoren für Unicode-Zeichenfolgen bereitstellt.
fscan
2
std :: string sollte Bytes speichern, dh die Codeeinheitssequenz der UTF-8-Codierung. Ja, genau das tut es von Anfang an. utf8everywhere.org
Pavel Radzivilovsky
3
Die größten potenziellen Probleme bei der Unicode-Unterstützung liegen bei Unicode und seiner Verwendung in der Informationstechnologie selbst. Unicode ist nicht für das geeignet (und nicht entwickelt), wofür es verwendet wird. Unicode wurde entwickelt, um jede mögliche Glyphe, die irgendwo von jemandem geschrieben wurde, zu einem bestimmten Zeitpunkt mit jeder unwahrscheinlichen und pedantischen Nuance zu reproduzieren, einschließlich 3 oder 4 verschiedener Bedeutungen und 3 oder 4 verschiedener Arten, dieselbe Glyphe zu komponieren. Es soll nicht nützlich sein, um für die Alltagssprache verwendet zu werden, und es soll nicht anwendbar sein oder einfach oder eindeutig verarbeitet werden.
Damon
11
Ja, es ist für die Verwendung in der Alltagssprache konzipiert. Meins zumindest. Und deine höchstwahrscheinlich auch. Es stellt sich nur heraus, dass die allgemeine Verarbeitung von menschlichem Text eine sehr schwierige Aufgabe ist. Es ist nicht einmal möglich, eindeutig zu definieren, was ein Charakter ist. Die allgemeine Glyphenwiedergabe ist nicht einmal wirklich Teil der Unicode-Charta.
Jean-Denis Muys

Antworten:

267

Wie gut unterstützt die C ++ - Standardbibliothek Unicode?

Fürchterlich.

Ein kurzer Scan durch die Bibliothekseinrichtungen, die möglicherweise Unicode-Unterstützung bieten, gibt mir folgende Liste:

  • Strings Bibliothek
  • Lokalisierungsbibliothek
  • Eingabe- / Ausgabebibliothek
  • Bibliothek für reguläre Ausdrücke

Ich denke, alle bis auf den ersten bieten schreckliche Unterstützung. Ich werde nach einem kurzen Umweg durch Ihre anderen Fragen ausführlicher darauf zurückkommen.

Tut std::stringwas es soll?

Ja. Nach dem C ++ - Standard sollten dies std::stringund seine Geschwister Folgendes tun:

Die Klassenvorlage basic_stringbeschreibt Objekte, die eine Sequenz speichern können, die aus einer variierenden Anzahl beliebiger char-ähnlicher Objekte besteht, wobei sich das erste Element der Sequenz an Position Null befindet.

Nun, std::stringmacht das gut. Bietet das Unicode-spezifische Funktionen? Nein.

Sollte es? Wahrscheinlich nicht. std::stringist gut als eine Folge von charObjekten. Das ist nützlich; Das einzige Ärgernis ist, dass es sich um eine sehr einfache Ansicht von Text handelt und Standard-C ++ keine übergeordnete Ansicht bietet.

Wie benutze ich es?

Verwenden Sie es als eine Folge von charObjekten; so zu tun, als wäre es etwas anderes, das mit Schmerzen enden muss.

Wo liegen mögliche Probleme?

Überall? Mal schauen...

Strings Bibliothek

Die Zeichenfolgenbibliothek bietet uns basic_stringlediglich eine Folge dessen, was der Standard "char-ähnliche Objekte" nennt. Ich nenne sie Codeeinheiten. Wenn Sie eine allgemeine Textansicht wünschen, ist dies nicht das, wonach Sie suchen. Dies ist eine Ansicht von Text, der für die Serialisierung / Deserialisierung / Speicherung geeignet ist.

Es enthält auch einige Tools aus der C-Bibliothek, mit denen die Lücke zwischen der engen Welt und der Unicode-Welt geschlossen werden kann: c16rtomb/ mbrtoc16und c32rtomb/ mbrtoc32.

Lokalisierungsbibliothek

Die Lokalisierungsbibliothek glaubt immer noch, dass eines dieser "char-ähnlichen Objekte" einem "Zeichen" entspricht. Dies ist natürlich albern und macht es unmöglich, viele Dinge über eine kleine Teilmenge von Unicode wie ASCII hinaus richtig zum Laufen zu bringen.

Überlegen Sie beispielsweise, was der Standard im <locale>Header als "Convenience-Schnittstellen" bezeichnet :

template <class charT> bool isspace (charT c, const locale& loc);
template <class charT> bool isprint (charT c, const locale& loc);
template <class charT> bool iscntrl (charT c, const locale& loc);
// ...
template <class charT> charT toupper(charT c, const locale& loc);
template <class charT> charT tolower(charT c, const locale& loc);
// ...

Wie erwarten Sie, dass eine dieser Funktionen beispielsweise U + 1F34C ʙᴀɴᴀɴᴀ wie in u8"🍌"oder richtig kategorisiert u8"\U0001F34C"? Es wird auf keinen Fall jemals funktionieren, da diese Funktionen nur eine Codeeinheit als Eingabe verwenden.

Dies könnte mit einem geeigneten Gebietsschema funktionieren, wenn Sie char32_tnur Folgendes verwenden : U'\U0001F34C'ist eine einzelne Codeeinheit in UTF-32.

Das bedeutet jedoch, dass nach wie vor nur Sie die einfachen Gehäuse Transformationen bekommen toupperund tolower, die, zum Beispiel, ist nicht gut genug für einige deutschen Gegenden: „ß“ uppercases auf „SS“ ☦ sondern toupperkann nur eine Rückzeichencodeeinheit.

Als nächstes wstring_convert/ wbuffer_convertund die Facetten der Standardcodekonvertierung.

wstring_convertwird verwendet, um zwischen Zeichenfolgen in einer bestimmten Codierung in Zeichenfolgen in einer anderen bestimmten Codierung umzuwandeln. An dieser Transformation sind zwei Zeichenfolgentypen beteiligt, die der Standard als Byte-Zeichenfolge und als breite Zeichenfolge bezeichnet. Da diese Begriffe wirklich irreführend sind, bevorzuge ich die Verwendung von "serialisiert" bzw. "deserialisiert" anstelle von †.

Die zu konvertierenden Codierungen werden von einem Codecvt (einer Codekonvertierungsfacette) festgelegt, der als Argument für den Vorlagentyp übergeben wird wstring_convert.

wbuffer_convertführt eine ähnliche Funktion aus, jedoch als breiter deserialisierter Stream-Puffer, der einen Byte- serialisierten Stream-Puffer umschließt. Alle E / A werden über den zugrunde liegenden Byte- serialisierten Stream-Puffer mit Konvertierungen zu und von den durch das Codecvt-Argument angegebenen Codierungen ausgeführt. Das Schreiben wird in diesen Puffer serialisiert und dann aus ihm geschrieben, und das Lesen liest in den Puffer und deserialisiert dann aus ihm.

Der Standard bietet einige codecvt Klassenvorlagen zur Verwendung mit diesen Einrichtungen: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16, und einige codecvtSpezialisierungen. Zusammen bieten diese Standardfacetten alle folgenden Konvertierungen. (Hinweis: In der folgenden Liste ist die Codierung links immer die serialisierte Zeichenfolge / Streambuf und die Codierung rechts immer die deserialisierte Zeichenfolge / Streambuf. Der Standard erlaubt Konvertierungen in beide Richtungen.)

  • UTF-8 ↔ UCS-2 mit codecvt_utf8<char16_t>und codecvt_utf8<wchar_t>wo sizeof(wchar_t) == 2;
  • UTF-8 ↔ UTF-32 codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t>und codecvt_utf8<wchar_t>wobei sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 mit codecvt_utf16<char16_t>und codecvt_utf16<wchar_t>wo sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 mit codecvt_utf16<char32_t>und codecvt_utf16<wchar_t>wo sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 mit codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t>und codecvt_utf8_utf16<wchar_t>wobei sizeof(wchar_t) == 2;
  • schmal ↔ breit mit codecvt<wchar_t, char_t, mbstate_t>
  • no-op mit codecvt<char, char, mbstate_t>.

Einige davon sind nützlich, aber hier gibt es viele unangenehme Dinge.

Zunächst einmal - heiliger hoher Ersatz! Dieses Namensschema ist chaotisch.

Dann gibt es viel UCS-2-Unterstützung. UCS-2 ist eine Codierung aus Unicode 1.0, die 1996 ersetzt wurde, da sie nur die mehrsprachige Grundebene unterstützt. Warum das Komitee es für wünschenswert hielt, sich auf eine Kodierung zu konzentrieren, die vor über 20 Jahren abgelöst wurde, weiß ich nicht ‡. Es ist nicht so, dass die Unterstützung für mehr Codierungen schlecht oder so ist, aber UCS-2 wird hier zu oft angezeigt.

Ich würde sagen, dass dies char16_toffensichtlich zum Speichern von UTF-16-Codeeinheiten gedacht ist. Dies ist jedoch ein Teil des Standards, der anders denkt. codecvt_utf8<char16_t>hat nichts mit UTF-16 zu tun. Zum Beispiel wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")wird kompilieren in Ordnung, aber fehl bedingungslos: der Eingang wird als UCS-2 - String behandelt werden u"\xD83C\xDF4C", die in UTF-8 konvertiert werden können , da UTF-8 nicht einen beliebigen Wert im Bereich 0xD800-0xDFFF kodieren kann.

Auf der UCS-2-Front gibt es noch keine Möglichkeit, mit diesen Facetten aus einem UTF-16-Byte-Stream in einen UTF-16-String zu lesen. Wenn Sie eine Folge von UTF-16-Bytes haben, können Sie diese nicht in eine Zeichenfolge von deserialisieren char16_t. Dies ist überraschend, da es sich mehr oder weniger um eine Identitätskonvertierung handelt. Noch überraschender ist jedoch die Tatsache, dass die Deserialisierung von einem UTF-16-Stream in einen UCS-2-String mit unterstützt wird codecvt_utf16<char16_t>, was eigentlich eine verlustbehaftete Konvertierung ist.

Die UTF-16-as-Byte-Unterstützung ist jedoch recht gut: Sie unterstützt das Erkennen von Endianess aus einer Stückliste oder das explizite Auswählen im Code. Es unterstützt auch die Erzeugung von Ausgaben mit und ohne Stückliste.

Es fehlen einige weitere interessante Konvertierungsmöglichkeiten. Es gibt keine Möglichkeit, von einem UTF-16-Byte-Stream oder einer Zeichenfolge in eine UTF-8-Zeichenfolge zu deserialisieren, da UTF-8 niemals als deserialisierte Form unterstützt wird.

Und hier ist die enge / weite Welt völlig getrennt von der UTF / UCS-Welt. Es gibt keine Konvertierungen zwischen den schmalen / breiten Codierungen im alten Stil und den Unicode-Codierungen.

Eingabe- / Ausgabebibliothek

Die E / A-Bibliothek kann zum Lesen und Schreiben von Text in Unicode-Codierungen unter Verwendung der oben beschriebenen wstring_convertund Funktionen verwendet wbuffer_convertwerden. Ich glaube nicht, dass dieser Teil der Standardbibliothek noch viel mehr unterstützen müsste.

Bibliothek für reguläre Ausdrücke

Ich habe bereits zuvor Probleme mit C ++ - Regexen und Unicode on Stack Overflow erläutert . Ich werde hier nicht alle diese Punkte wiederholen, sondern lediglich angeben, dass C ++ - Regexes keine Unicode-Unterstützung der Stufe 1 bieten. Dies ist das absolute Minimum, um sie nutzbar zu machen, ohne überall UTF-32 zu verwenden.

Das ist es?

Ja das ist es. Das ist die vorhandene Funktionalität. Es gibt viele Unicode-Funktionen, die wie Normalisierungs- oder Textsegmentierungsalgorithmen nirgends zu sehen sind.

U + 1F4A9 . Gibt es eine Möglichkeit, eine bessere Unicode-Unterstützung in C ++ zu erhalten?

Die üblichen Verdächtigen: ICU und Boost.Locale .


† Eine Byte-Zeichenfolge ist nicht überraschend eine Zeichenfolge von Bytes, dh charObjekten. Im Gegensatz zu einem breiten Zeichenfolgenliteral , das immer ein Array von wchar_tObjekten ist, ist eine "breite Zeichenfolge" in diesem Zusammenhang jedoch nicht unbedingt eine Zeichenfolge von wchar_tObjekten. Tatsächlich definiert der Standard niemals explizit, was eine "breite Zeichenfolge" bedeutet, so dass wir die Bedeutung der Verwendung erraten müssen. Da die Standardterminologie schlampig und verwirrend ist, verwende ich meine eigene im Namen der Klarheit.

Codierungen wie UTF-16 können als Sequenzen von gespeichert werden char16_t, die dann keine Endianness haben; oder sie können als Folgen von Bytes gespeichert werden, die Endianness haben (jedes aufeinanderfolgende Bytepaar kann char16_tje nach Endianness einen anderen Wert darstellen). Der Standard unterstützt beide Formen. Eine Folge von char16_tist für die interne Manipulation im Programm nützlicher. Eine Folge von Bytes ist der Weg, solche Zeichenfolgen mit der Außenwelt auszutauschen. Die Begriffe, die ich anstelle von "Byte" und "Wide" verwenden werde, sind daher "serialisiert" und "deserialisiert".

‡ Wenn Sie "aber Windows!" halte dein 🐎🐎 . Alle Windows-Versionen seit Windows 2000 verwenden UTF-16.

☦ Ja, ich kenne das große Eszett (ẞ), aber selbst wenn Sie über Nacht alle deutschen Gebietsschemas ändern würden, um Großbuchstaben in ẞ zu setzen, gibt es noch viele andere Fälle, in denen dies fehlschlagen würde. Versuchen Sie, U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ in Großbuchstaben zu schreiben. Es gibt kein ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; es werden nur Großbuchstaben auf zwei Fs gesetzt. Oder U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴊ ᴄᴀʀᴏɴ; Es gibt kein vorkomponiertes Kapital. Es werden nur Großbuchstaben zu einem Großbuchstaben J und einem kombinierten Caron verwendet.

R. Martinho Fernandes
quelle
26
Je mehr ich darüber las, desto mehr hatte ich das Gefühl, nichts davon zu verstehen. Ich habe das meiste vor ein paar Monaten gelesen und habe immer noch das Gefühl, dass ich das Ganze noch einmal entdecke ... Um es für mein armes Gehirn, das jetzt ein bisschen weh tut, einfach zu halten, sind all diese Ratschläge zu utf8everywhere immer noch gültig. richtig? Wenn ich "nur" möchte, dass meine Benutzer Dateien unabhängig von ihren Systemeinstellungen öffnen und schreiben können, kann ich sie nach dem Dateinamen fragen, ihn in einem std :: string speichern und alles sollte auch unter Windows ordnungsgemäß funktionieren? Tut mir leid, das (noch einmal) zu fragen ...
Uflex
5
@Uflex Alles, was Sie mit std :: string wirklich tun können, ist, es als binären Blob zu behandeln. In einer ordnungsgemäßen Unicode-Implementierung ist weder die interne (weil sie tief in den Implementierungsdetails verborgen ist) noch die externe Codierung von Bedeutung (na ja, sorta, Sie müssen immer noch Encoder / Decoder zur Verfügung haben).
Cat Plus Plus
3
@ Uflex vielleicht. Ich weiß nicht, ob es eine gute Idee ist, Ratschlägen zu folgen, die Sie nicht verstehen.
R. Martinho Fernandes
1
Es gibt einen Vorschlag für die Unicode-Unterstützung in C ++ 2014/17. Das ist jedoch 1, vielleicht 4 Jahre entfernt und jetzt von geringem Nutzen. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds
20
@ graham.reeds haha, danke, aber das war mir bewusst. Überprüfen Sie den Abschnitt "Danksagungen";)
R. Martinho Fernandes
40

Unicode wird von der Standardbibliothek nicht unterstützt (für eine vernünftige Bedeutung von unterstützt).

std::stringist nicht besser als std::vector<char>: Unicode (oder jede andere Darstellung / Codierung) wird nicht wahrgenommen und der Inhalt wird einfach als Byte- Blob behandelt .

Wenn Sie nur Blobs speichern und verketten müssen , funktioniert dies ziemlich gut. Sobald Sie jedoch die Unicode-Funktionalität (Anzahl der Codepunkte , Anzahl der Grapheme usw.) wünschen, haben Sie kein Glück mehr.

Die einzige umfassende Bibliothek, die mir dafür bekannt ist, ist die Intensivstation . Die C ++ - Schnittstelle wurde jedoch von der Java-Schnittstelle abgeleitet, sodass sie keineswegs idiomatisch ist.

Matthieu M.
quelle
2
Wie wäre es mit Boost.Locale ?
Uflex
11
@Uflex: von der Seite, die Sie verlinkt haben Um dieses Ziel zu erreichen, verwendet Boost.Locale die hochmoderne Unicode- und Lokalisierungsbibliothek: ICU - International Components for Unicode.
Matthieu M.
1
Boost.Locale unterstützt andere Backends außerhalb der Intensivstation, siehe hier: boost.org/doc/libs/1_53_0/libs/locale/doc/html/…
Superfly Jon
@SuperflyJon: Stimmt, aber laut derselben Seite ist die Unterstützung für Unicode der Nicht-ICU-Backends "stark eingeschränkt".
Matthieu M.
24

Sie können UTF-8 sicher in einem std::string(oder in einem char[]oder char*) speichern , da ein Unicode-NUL (U + 0000) in UTF-8 ein Null-Byte ist und dies der einzige Weg ist, eine Null zu sein Byte kann in UTF-8 auftreten. Daher werden Ihre UTF-8-Zeichenfolgen gemäß allen C- und C ++ - Zeichenfolgenfunktionen ordnungsgemäß terminiert, und Sie können sie mit C ++ - iostreams (einschließlich std::coutund std::cerr, solange Ihr Gebietsschema UTF-8 ist) herumschleudern.

Was Sie std::stringfür UTF-8 nicht tun können, ist die Länge in Codepunkten abzurufen. std::string::size()zeigt Ihnen die Zeichenfolgenlänge in Bytes an , die nur der Anzahl der Codepunkte entspricht, wenn Sie sich in der ASCII-Teilmenge von UTF-8 befinden.

Wenn Sie UTF-8-Zeichenfolgen auf Codepunktebene bearbeiten müssen (dh nicht nur speichern und drucken müssen) oder wenn Sie mit UTF-16 arbeiten, das wahrscheinlich viele interne Nullbytes enthält, müssen Sie dies untersuchen die breiten Zeichenkettentypen.

uckelman
quelle
3
std::stringkann mit eingebetteten Nullen in iostreams geworfen werden.
R. Martinho Fernandes
3
Es ist total beabsichtigt. Es bricht überhaupt nicht, c_str()weil es size()immer noch funktioniert. Nur defekte APIs (dh solche, die nicht wie die meisten in der C-Welt mit eingebetteten Nullen umgehen können) brechen.
R. Martinho Fernandes
1
Eingebettete Nullen werden unterbrochen, c_str()weil c_str()die Daten als nullterminierte C-Zeichenfolge zurückgegeben werden sollen. Dies ist unmöglich, da C-Zeichenfolgen keine eingebetteten Nullen enthalten können.
uckelman
4
Nicht länger. c_str()Jetzt wird einfach das Gleiche zurückgegeben wie data(), dh alles. APIs, die eine Größe annehmen, können diese verbrauchen. APIs, die dies nicht tun, können dies nicht.
R. Martinho Fernandes
6
Mit dem kleinen Unterschied, c_str()der sicherstellt , dass auf das Ergebnis ein NUL-Zeichen-ähnliches Objekt folgt, und ich glaube nicht data(). Nein, sieht so aus, als würde das data()jetzt auch so sein. (Dies ist natürlich nicht erforderlich für APIs, die die Größe verbrauchen, anstatt sie aus einer Terminatorsuche abzuleiten.)
Ben Voigt
8

C ++ 11 verfügt über einige neue Literal-String-Typen für Unicode.

Leider ist die Unterstützung in der Standardbibliothek für ungleichmäßige Codierungen (wie UTF-8) immer noch schlecht. Zum Beispiel gibt es keine gute Möglichkeit, die Länge (in Codepunkten) eines UTF-8-Strings zu ermitteln.

Ein Programmierer
quelle
Müssen wir also immer noch std :: wstring für Dateinamen verwenden, wenn wir nicht-lateinische Sprachen unterstützen möchten? Weil die neuen String-Literale hier nicht wirklich helfen, da der String normalerweise vom Benutzer stammt ...
Uflex
7
@Uflex std::stringkann halten einen UTF-8 - String , ohne Problem, aber die zB lengthMethode gibt die Anzahl der Bytes in der Zeichenfolge und nicht die Anzahl der Codepunkte.
Einige Programmierer Typ
8
Um ehrlich zu sein, hat das Abrufen der Länge in Codepunkten einer Zeichenfolge nicht viele Verwendungszwecke. Die Länge in Bytes kann zum Beispiel verwendet werden, um Puffer korrekt vorzuordnen.
R. Martinho Fernandes
2
Die Anzahl der Codepunkte in einer UTF-8-Zeichenfolge ist keine sehr interessante Zahl: Man kann schreiben ñals 'LATEINISCHER KLEINBUCHSTABE N MIT TILD' (U + 00F1) (das ist ein Codepunkt) oder 'LATEINISCHER KLEINBUCHSTABE N' ( U + 006E) gefolgt von 'COMBINING TILDE' (U + 0303), zwei Codepunkten.
Martin Bonner unterstützt Monica
All diese Kommentare zu "Sie brauchen das nicht und Sie brauchen das nicht" wie "Anzahl der unwichtigen Codepunkte" usw. klingen für mich etwas faul. Sobald Sie einen Parser geschrieben haben, der den utf8-Quellcode analysieren soll, hängt es von der Spezifikation des Parsers ab, ob er LATIN SMALL LETTER N' == berücksichtigt oder nicht (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler
4

Es gibt jedoch eine ziemlich nützliche Bibliothek namens tiny-utf8 , die im Grunde ein Drop-In-Ersatz für std::string/ ist std::wstring. Ziel ist es, die Lücke der noch fehlenden utf8-string-Containerklasse zu schließen.

Dies ist möglicherweise die bequemste Art, mit utf8-Zeichenfolgen umzugehen (dh ohne Unicode-Normalisierung und ähnliches). Sie können Codepunkte bequem bearbeiten , während Ihre Zeichenfolge in Lauflängen-codierten chars codiert bleibt .

Jakob Riedle
quelle