Wie verwende ich std :: string unter UTF-8 in C ++ richtig?

78

Meine Plattform ist ein Mac und C ++ 11 (oder höher). Ich bin ein C ++ - Anfänger und arbeite an einem persönlichen Projekt, das Chinesisch und Englisch verarbeitet. UTF-8 ist die bevorzugte Codierung für dieses Projekt.

Ich habe einige Beiträge zu Stack Overflow gelesen, und viele von ihnen schlagen vor, sie std::stringbeim Umgang mit UTF-8 zu verwenden und zu vermeiden, wchar_tda es derzeit keine char8_tfür UTF-8 gibt.

Doch keiner von ihnen darüber sprechen , wie man richtig mit Funktionen umgehen wie str[i], std::string::size(), std::string::find_first_of()oder , std::regexda diese Funktion gibt in der Regel unerwartete Ergebnisse , wenn UTF-8 gegenüber .

Soll ich weitermachen std::stringoder zu wechseln std::wstring? Wenn ich bei bleiben sollte std::string, was ist die beste Vorgehensweise, um die oben genannten Probleme zu lösen?

Stackunderflow
quelle
13
Siehe auch utf8everywhere
Caleth
3
Warum (und wie std::wstring?! ) Würden Sie mit UTF-8 verwenden?
Jonathan Wakely
6
std::string::size()ist nur dann überraschend, wenn Sie erwarten, dass Sie etwas anderes tun, als die Länge in Bytes zurückzugeben, dh Codeeinheiten (nicht die Anzahl der Codepunkte in der Zeichenfolge). Und str[i]gibt das i-te Byte in der Zeichenfolge zurück. Dies wäre jedoch auch dann der Fall, wenn C ++ einen char8_tTyp speziell für UTF-8 hätte.
Jonathan Wakely
Dies mag ein wenig vom Thema abweichen, aber warum C ++? Apple ist ein Bürger zweiter Klasse auf dem Mac. Apple bietet viel bessere Unterstützung für Objective-C und (in jüngerer Zeit) Swift. Auf der Grundlage , dass es klingt wie Sie eine Befehlszeile app schreiben, könnten Sie einen Blick auf nehmen diese . Dann können Sie aufhören, sich Gedanken über die beschissene Unterstützung von C ++ für Unicode zu machen, und mit dem Schreiben Ihres Programms fortfahren. Google swift unicodeund swift regexalles ist für Sie erledigt.
Paul Sanders
PS: Was macht das Programm tatsächlich tun ?
Paul Sanders

Antworten:

113

Unicode-Glossar

Unicode ist ein umfangreiches und komplexes Thema. Ich möchte dort nicht zu tief waten, aber ein kurzes Glossar ist notwendig:

  1. Punkte Code : Code Punkte sind die grundlegenden Bausteine von Unicode, ein Codepunkt ist nur eine ganze Zahl auf einem zugeordneten Bedeutung . Der ganzzahlige Teil passt in 32 Bit (also wirklich 24 Bit), und die Bedeutung kann ein Buchstabe, ein diakritischer Wert, ein Leerzeichen, ein Zeichen, ein Smiley, eine halbe Flagge usw. sein, und es kann sogar "the" sein nächster Teil lautet von rechts nach links ".
  2. Graphemcluster : Graphemcluster sind Gruppen von semantisch verwandten Codepunkten. Beispielsweise wird ein Flag in Unicode durch Zuordnen von zwei Codepunkten dargestellt. Jede dieser beiden hat für sich genommen keine Bedeutung, ist jedoch in einem Graphemcluster miteinander verbunden und repräsentiert ein Flag. Graphemcluster werden in einigen Skripten auch verwendet, um einen Buchstaben mit einem diakritischen Zeichen zu koppeln.

Dies ist die Basis von Unicode. Die Unterscheidung zwischen Codepunkt und Graphemcluster kann größtenteils beschönigt werden, da für die meisten modernen Sprachen jedes "Zeichen" einem einzelnen Codepunkt zugeordnet ist (es gibt spezielle akzentuierte Formen für häufig verwendete Buchstaben- / diakritische Kombinationen). Wenn Sie sich jedoch in Smileys, Flaggen usw. wagen, müssen Sie möglicherweise auf die Unterscheidung achten.


UTF-Grundierung

Dann muss eine Reihe von Unicode-Codepunkten codiert werden. Die gemeinsamen Codierungen sind UTF-8, UTF-16 und UTF-32, wobei die beiden letzteren sowohl in Little-Endian- als auch in Big-Endian-Form existieren, für insgesamt 5 gemeinsame Codierungen.

In UTF-X, X die Größe in Bits der Code Einheit , jeder Code Point als eine oder mehrere Kode Units in Abhängigkeit von ihrer Größe dargestellt wird:

  • UTF-8: 1 bis 4 Codeeinheiten,
  • UTF-16: 1 oder 2 Codeeinheiten,
  • UTF-32: 1 Codeeinheit.

std::stringund std::wstring.

  1. Nicht verwenden, std::wstringwenn Sie Wert auf Portabilität legen ( wchar_tunter Windows sind es nur 16 Bit). benutze std::u32stringstattdessen (aka std::basic_string<char32_t>).
  2. Die speicherinterne Darstellung ( std::stringoder std::wstring) ist unabhängig von der Darstellung auf der Festplatte (UTF-8, UTF-16 oder UTF-32). Bereiten Sie sich also darauf vor, dass Sie an der Grenze konvertieren müssen (Lesen und Schreiben).
  3. Während ein 32-Bit wchar_tsicherstellt, dass eine Codeeinheit einen vollständigen Codepunkt darstellt, repräsentiert sie immer noch keinen vollständigen Graphemcluster.

Wenn Sie nur Zeichenfolgen lesen oder komponieren, sollten Sie keine oder nur geringe Probleme mit std::stringoder haben std::wstring.

Probleme beginnen, wenn Sie mit dem Schneiden und Würfeln beginnen. Dann müssen Sie auf (1) Codepunktgrenzen (in UTF-8 oder UTF-16) und (2) Graphemclustergrenzen achten. Ersteres kann problemlos selbst gehandhabt werden, letzteres erfordert die Verwendung einer Unicode-fähigen Bibliothek.


Pflücken std::stringoder std::u32string?

Wenn die Leistung ein Problem darstellt, ist es wahrscheinlich, dass std::stringdie Leistung aufgrund des geringeren Speicherbedarfs besser ist. obwohl starker Gebrauch von Chinesisch das Geschäft ändern kann. Wie immer Profil.

Wenn Graphemcluster kein Problem darstellen, std::u32stringhat dies den Vorteil, dass die Dinge vereinfacht werden: 1 Codeeinheit -> 1 Codepunkt bedeutet, dass Sie Codepunkte nicht versehentlich aufteilen können und alle Funktionen sofort einsatzbereit sind std::basic_string.

Wenn Sie mit Software verbunden sind std::stringoder char*/ char const*, halten Sie sich an std::string, um hin und her Konvertierungen zu vermeiden. Sonst wird es ein Schmerz sein.


UTF-8 in std::string.

UTF-8 funktioniert eigentlich ganz gut in std::string.

Die meisten Vorgänge funktionieren sofort, da die UTF-8-Codierung selbstsynchronisiert und abwärtskompatibel mit ASCII ist.

Aufgrund der Art und Weise, wie Codepunkte codiert werden, kann die Suche nach einem Codepunkt nicht versehentlich mit der Mitte eines anderen Codepunkts übereinstimmen:

  • str.find('\n') funktioniert,
  • str.find("...")funktioniert für den Abgleich von Byte für Byte 1 ,
  • str.find_first_of("\r\n")funktioniert bei der Suche nach ASCII-Zeichen .

Ebenso regexsollte meistens sofort funktionieren. Da eine Folge von Zeichen ( "haha") nur eine Folge von Bytes ( "哈") ist, sollten grundlegende Suchmuster sofort funktionieren.

Seien Sie jedoch vorsichtig bei Zeichenklassen (z. B. [:alphanum:]), da diese je nach Regex-Geschmack und Implementierung möglicherweise mit Unicode-Zeichen übereinstimmen oder nicht.

Seien Sie in ähnlicher Weise vorsichtig, wenn Sie Repeater auf Nicht-ASCII- "Zeichen" anwenden, und betrachten Sie "哈?"möglicherweise nur das letzte Byte als optional. Verwenden Sie Klammern, um die wiederholte Folge von Bytes in solchen Fällen klar abzugrenzen : "(哈)?".

1 Die Schlüsselkonzepte für die Suche sind Normalisierung und Sortierung. Dies betrifft alle Vergleichsoperationen. std::stringvergleicht (und sortiert) immer Byte für Byte, ohne Rücksicht auf sprach- oder verwendungsspezifische Vergleichsregeln. Wenn Sie die vollständige Normalisierung / Sortierung durchführen möchten, benötigen Sie eine vollständige Unicode-Bibliothek, z. B. die Intensivstation.

Matthieu M.
quelle
Danke für die tollen Details! Ich versuche mir etwas Zeit zu nehmen, um all das herauszufinden! Über die ursprünglichen Fragen, neben str.find_first_of, str.findoder std::regexscheint nicht Arbeit für nicht ASCII - Eingänge (zB „哈“ oder u8 „哈“) gegebenstd::string str(u8"哈哈haha");
Stackunderflow
4
@Edityouprofile: str.find("哈")sollte funktionieren (siehe ideone.com/s9i1yf ), wird aber str.find('哈')nicht, weil '哈'es sich um ein Multi-Byte-Zeichen handelt. str.find_first_of("哈")funktioniert nicht (funktioniert nur für ASCII-Muster). Regex sollte für ASCII-Muster gut funktionieren. Achten Sie jedoch auf Zeichenklassen und "Repeater" (z. B. "哈?"darf nur das letzte Byte bedingt sein).
Matthieu M.
1
Würde aus Gründen der Portabilität std::basic_string<char32_t>sowohl unter * nix als auch unter Windows wie erwartet funktionieren?
Quentin
1
@ Quentin: Ja. Ich sollte es der Liste der Alternativen hinzufügen! Übrigens gibt es ein schickes typedef : std::u32string.
Matthieu M.
1
str.find("...")str.fin worksNur wenn Sie sich nur darum kümmern, Byte für Byte abzugleichen - andernfalls benötigen Sie einen ordnungsgemäßen Vergleich, der die Normalisierung und das Gebietsschema berücksichtigt. Davon abgesehen scheint dies eine ziemlich gute Antwort zu sein und zeigt, warum ich die Unicode- "Unterstützung", die in Sprachen wie Python3 existiert, irgendwie hasse.
Muzer
10

std::stringund Freunde sind kodierungsunabhängig. Der einzige Unterschied zwischen std::wstringund std::stringsind , dass std::wstringVerwendungen wchar_tals Einzelelement, nicht char. Für die meisten Compiler ist letzteres 8-Bit. Ersteres soll groß genug sein, um Unicode-Zeichen aufzunehmen, in der Praxis jedoch auf einigen Systemen nicht (der Microsoft-Compiler verwendet beispielsweise einen 16-Bit-Typ). Sie können UTF-8 nicht in speichern std::wstring. Dafür ist es nicht gedacht. Es entspricht UTF-32 - einer Zeichenfolge, bei der jedes Element ein einzelner Unicode-Codepunkt ist.

Wenn Sie UTF-8-Zeichenfolgen nach Unicode-Codepunkt oder zusammengesetztem Unicode-Glyphen (oder einem anderen Element) indizieren möchten, zählen Sie die Länge einer UTF-8-Zeichenfolge in Unicode-Codepunkten oder einem anderen Unicode-Objekt oder suchen Sie nach Unicode-Codepunkt Sie müssen etwas anderes als die Standardbibliothek verwenden. Die Intensivstation ist eine der Bibliotheken auf diesem Gebiet. es kann andere geben.

Bemerkenswert ist wahrscheinlich, dass Sie bei der Suche nach ASCII-Zeichen einen UTF-8-Bytestream meistens so behandeln können, als wäre er Byte für Byte. Jedes ASCII-Zeichen codiert in UTF-8 dasselbe wie in ASCII, und jede Multi-Byte-Einheit in UTF-8 enthält garantiert keine Bytes im ASCII-Bereich.

James Picone
quelle
3
"Unterschiedliche Codes für alle Mitglieder des größten erweiterten Zeichensatzes" bedeutet, dass ein einzelnes wchar_t einen gültigen Unicode-Codepunkt darstellen kann, wenn Ihr Compiler Unicode unterstützt. 16 Bit reichen dafür nicht aus. UTF-16 ist eine Multi-Byte-Codierung. es ist hier nicht relevant.
James Picone
6
Der Schaden ist, dass es sich std::wstringeigentlich nicht um eine Multi-Byte-Codierung handeln sollte. das ist der Punkt des Typs. Das Erstellen einer Multi-Byte-Codierung (und einer schlechten) ist nur ein Duplizieren std::string, aber auf eine wirklich nervige Weise, die die Leute dazu bringt, zu glauben, dass ihr Code Unicode richtig macht.
James Picone
11
@zneak Es ist eigentlich Unicodes Schuld, nicht die von Microsoft. Sie sagten Microsoft, dass Zeichen 16-Bit waren, dann ging Microsoft und machte sie 16-Bit, dann sagten sie "Ups, nein, sie müssen 20,5-Bit sein". Der einzige Grund, warum * nixes nicht das gleiche Problem haben, ist, dass sie Unicode erst unterstützt haben, nachdem die 20,5-Bit-Entscheidung getroffen wurde.
user253751
4
@zneak UTF-32 ist keine Multi-Byte-Codierung wie UTF-16. UTF-16 erfordert manchmal mehrere Werte, um einzelne Unicode-Codepunkte darzustellen. UTF-32 erfordert manchmal mehrere Unicode-Codepunkte, um einzelne Grapheme darzustellen. Sie sind beide knifflig, aber auf verschiedenen Ebenen knifflig.
James Picone
8
@ JamesPicone: "Codierung mit variabler Breite" ist wahrscheinlich ein geeigneterer Begriff als "Multi-Byte-Codierung".
user2357112 unterstützt Monica
8

Beide std::string und std::wstringmüssen UTF-Codierung verwenden, um Unicode darzustellen. Unter macOS std::stringist UTF-8 (8-Bit-Codeeinheiten) und std::wstring UTF-32 (32-Bit-Codeeinheiten). Beachten Sie, dass die Größe von wchar_tplattformabhängig ist.

Verfolgt für beide sizedie Anzahl der Codeeinheiten anstelle der Anzahl der Codepunkte oder Graphemcluster. (Ein Codepunkt ist eine Unicode-Entität, von der einer oder mehrere einen Graphemcluster bilden. Graphemcluster sind die sichtbaren Zeichen, mit denen Benutzer interagieren, z. B. Buchstaben oder Emojis.)

Obwohl ich mit der Unicode-Darstellung von Chinesisch nicht vertraut bin, ist es sehr wahrscheinlich, dass bei Verwendung von UTF-32 die Anzahl der Codeeinheiten häufig sehr nahe an der Anzahl der Graphemcluster liegt. Dies geht natürlich zu Lasten des bis zu vierfachen Speicherbedarfs.

Die genaueste Lösung wäre die Verwendung einer Unicode-Bibliothek wie z. B. der Intensivstation, um die gewünschten Unicode-Eigenschaften zu berechnen.

Schließlich eignen sich UTF-Zeichenfolgen in menschlichen Sprachen, in denen keine Zeichen kombiniert werden, normalerweise recht gut mit find/ regex. Ich bin mir nicht sicher über Chinesisch, aber Englisch ist einer von ihnen.

zneak
quelle
2
Danke für deine Antwort. Während std::string str(u8"哈哈haha");str.find_first_of(u8"haha");scheint zu funktionieren, geben Sie str.find_first_of(u8"哈ha");immer 0 zurück. Und Regex scheint auch nicht zu funktionieren.
Stackunderflow
1
@Edityouprofile, das ist mein Fehler: Ich verwirrte find_first_ofmit find. find_first_ofkann nicht mit Multi-Byte-Zeichen arbeiten.
Zneak
11
" Verfolgt für beide sizedie Anzahl der Codepunkte " - falsch, es repräsentiert Codeeinheiten , keine Codepunkte . Großer Unterschied. " anstelle der Anzahl der logischen Zeichen. (Logische Zeichen sind ein oder mehrere Codepunkte.) " - auch formal als Graphemcluster bekannt.
Remy Lebeau
2
Ich glaube nicht , dass die Norm erfordert std::string in UTF8 sein, auch wenn wir zu neigen UTF8 überall . Ich vermute, dass ein EBCDIC-Mainframe EBCDIC fürstd::string
Basile Starynkevitch
13
std::string"verwendet" keine Codierung, weder UTF-8 noch EBCDIC. std::stringist nur ein Container für Bytes von Typen char. Sie können dort UTF-8-Zeichenfolgen oder ASCII-Zeichenfolgen oder EBCDIC-Zeichenfolgen oder sogar Binärdaten einfügen. Die Codierung dieser Bytes (falls vorhanden) wird vom Rest Ihres Programms bestimmt und davon, was Sie mit der Zeichenfolge tun, nicht von std::stringselbst.
Jonathan Wakely
5

Erwägen Sie ein Upgrade auf C ++ 20, und std::u8stringdas ist das Beste, was wir ab 2019 für das Halten von UTF-8 haben. Es gibt keine Standardbibliotheksfunktionen für den Zugriff auf einzelne Codepunkte oder Graphemcluster, aber zumindest ist Ihr Typ stark genug, um zumindest zu sagen, dass es sich um echtes UTF-8 handelt.


quelle