Dans quelle mesure Unicode est-il pris en charge dans C ++ 11?

183

J'ai lu et entendu que C ++ 11 prend en charge Unicode. Quelques questions à ce sujet:

  • Dans quelle mesure la bibliothèque standard C ++ prend-elle en charge Unicode?
  • Fait std::stringce qu'il doit faire?
  • Comment l'utiliser?
  • Où sont les problèmes potentiels?
Ralph Tandetzky
la source
19
"Est-ce que std :: string fait ce qu'il devrait?" Que pensez-vous qu'il devrait faire?
R. Martinho Fernandes
2
J'utilise utfcpp.sourceforge.net pour mes besoins utf8. C'est un simple fichier d'en-tête qui fournit des itérateurs pour les chaînes Unicode.
fscan le
2
std :: string devrait stocker des octets, c'est-à-dire une séquence d'unité de code du codage UTF-8. Oui, c'est exactement cela, depuis le début. utf8everywhere.org
Pavel Radzivilovsky
3
Les plus gros problèmes potentiels avec la prise en charge d'Unicode résident dans Unicode et son utilisation dans la technologie de l'information elle-même. Unicode n'est pas adapté (et n'est pas conçu) pour ce pour quoi il est utilisé. Unicode est conçu pour reproduire tous les glyphes possibles qui ont été écrits quelque part par quelqu'un, à un moment donné avec toutes les nuances improbables et pédantes possibles, y compris 3 ou 4 significations différentes et 3 ou 4 façons différentes de composer le même glyphe. Il n'est pas destiné à être utile pour être utilisé pour le langage courant, et il n'est pas destiné à être applicable ou à être traité facilement ou sans ambiguïté.
Damon
11
Oui, il est conçu pour être utilisé pour le langage courant. Le mien au moins. Et le vôtre probablement aussi. Il s'avère simplement que le traitement d'un texte humain d'une manière générale est une tâche très difficile. Il n'est même pas possible de définir sans ambiguïté ce qu'est un personnage. La reproduction générale des glyphes ne fait même pas vraiment partie de la charte Unicode.
Jean-Denis Muys

Réponses:

267

Dans quelle mesure la bibliothèque standard C ++ prend-elle en charge l'Unicode?

Terriblement.

Une analyse rapide des fonctionnalités de la bibliothèque susceptibles de fournir un support Unicode me donne cette liste:

  • Bibliothèque de chaînes
  • Bibliothèque de localisation
  • Bibliothèque d'entrée / sortie
  • Bibliothèque d'expressions régulières

Je pense que tous, sauf le premier, fournissent un soutien terrible. J'y reviendrai plus en détail après un petit détour par vos autres questions.

Fait std::stringce qu'il doit faire?

Oui. Selon la norme C ++, voici ce que std::stringses frères et sœurs devraient faire:

Le modèle de classe basic_stringdécrit des objets qui peuvent stocker une séquence constituée d'un nombre variable d'objets arbitraires de type char avec le premier élément de la séquence à la position zéro.

Eh bien, std::stringça va très bien. Cela fournit-il des fonctionnalités spécifiques à Unicode? Non.

Devrait-il? Probablement pas. std::stringest bien comme une séquence d' charobjets. C'est utile; le seul inconvénient est qu'il s'agit d'une vue de texte de très bas niveau et que le C ++ standard n'en fournit pas une de plus haut niveau.

Comment l'utiliser?

Utilisez-le comme une séquence d' charobjets; prétendre que c'est quelque chose d'autre ne peut que se terminer par la douleur.

Où sont les problèmes potentiels?

Partout? Voyons voir...

Bibliothèque de chaînes

La bibliothèque de chaînes nous fournit basic_string, qui est simplement une séquence de ce que le standard appelle des "objets de type char". Je les appelle des unités de code. Si vous voulez une vue de haut niveau du texte, ce n'est pas ce que vous recherchez. Il s'agit d'une vue de texte adaptée à la sérialisation / désérialisation / stockage.

Il fournit également quelques outils de la bibliothèque C qui peuvent être utilisés pour combler le fossé entre le monde étroit et le monde Unicode: c16rtomb/ mbrtoc16et c32rtomb/ mbrtoc32.

Bibliothèque de localisation

La bibliothèque de localisation pense toujours qu'un de ces "objets de type char" équivaut à un "caractère". Ceci est bien sûr ridicule et rend impossible le bon fonctionnement de beaucoup de choses au-delà d'un petit sous-ensemble d'Unicode comme ASCII.

Considérez, par exemple, ce que la norme appelle «interfaces de commodité» dans l'en- <locale>tête:

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);
// ...

Comment vous attendez-vous à ce que l'une de ces fonctions catégorise correctement, par exemple, U + 1F34C ʙᴀɴᴀɴᴀ, comme dans u8"🍌"ou u8"\U0001F34C"? Cela ne fonctionnera jamais, car ces fonctions ne prennent qu'une seule unité de code en entrée.

Cela pourrait fonctionner avec une locale appropriée si vous n'utilisiez char32_tque: U'\U0001F34C'est une unité de code unique en UTF-32.

Cependant, cela signifie toujours que vous n'obtenez que les transformations de casse simples avec toupperet tolower, qui, par exemple, ne sont pas assez bonnes pour certains paramètres régionaux allemands: "ß" majuscules en "SS" ☦ mais toupperne peut renvoyer qu'une unité de code de caractère .

Ensuite, wstring_convert/ wbuffer_convertet les facettes de conversion de code standard.

wstring_convertest utilisé pour convertir des chaînes d'un codage donné en chaînes d'un autre codage donné. Il existe deux types de chaîne impliqués dans cette transformation, que la norme appelle une chaîne d'octets et une chaîne large. Puisque ces termes sont vraiment trompeurs, je préfère utiliser respectivement "sérialisé" et "désérialisé" †.

Les codages à convertir sont décidés par un codecvt (une facette de conversion de code) passé en tant qu'argument de type de modèle à wstring_convert.

wbuffer_convertexécute une fonction similaire, mais en tant que tampon de flux désérialisé large qui enveloppe un tampon de flux sérialisé d' octets . Toutes les E / S sont effectuées via le tampon de flux sérialisé d' octets sous-jacent avec des conversions vers et à partir des encodages donnés par l'argument codecvt. L'écriture sérialise dans ce tampon, puis écrit à partir de celui-ci, et la lecture lit dans le tampon, puis désérialise à partir de celui-ci.

La norme fournit des modèles de classe codecvt pour une utilisation avec ces installations: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16, et certaines codecvtspécialisations. Ensemble, ces facettes standard fournissent toutes les conversions suivantes. (Remarque: dans la liste suivante, l'encodage à gauche est toujours la chaîne sérialisée / streambuf, et l'encodage à droite est toujours la chaîne désérialisée / streambuf; le standard autorise les conversions dans les deux sens).

  • UTF-8 ↔ UCS-2 avec codecvt_utf8<char16_t>et codecvt_utf8<wchar_t>sizeof(wchar_t) == 2;
  • UTF-32 avec UTF-8 ↔ codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t>et codecvt_utf8<wchar_t>sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 avec codecvt_utf16<char16_t>et codecvt_utf16<wchar_t>sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 avec codecvt_utf16<char32_t>et codecvt_utf16<wchar_t>sizeof(wchar_t) == 4;
  • UTF-16 avec UTF-8 ↔ codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t>et codecvt_utf8_utf16<wchar_t>sizeof(wchar_t) == 2;
  • étroit ↔ large avec codecvt<wchar_t, char_t, mbstate_t>
  • no-op avec codecvt<char, char, mbstate_t>.

Plusieurs d'entre eux sont utiles, mais il y a beaucoup de choses gênantes ici.

Tout d'abord - saint substitut élevé! ce schéma de dénomination est compliqué.

Ensuite, il y a beaucoup de support UCS-2. UCS-2 est un encodage d'Unicode 1.0 qui a été remplacé en 1996 car il ne prend en charge que le plan multilingue de base. Je ne sais pas pourquoi le comité a jugé souhaitable de se concentrer sur un codage qui a été remplacé il y a plus de 20 ans. Ce n'est pas comme si le support pour plus d'encodages était mauvais ou quoi que ce soit, mais UCS-2 apparaît trop souvent ici.

Je dirais que char16_tc'est évidemment destiné au stockage des unités de code UTF-16. Cependant, c'est une partie de la norme qui pense autrement. codecvt_utf8<char16_t>n'a rien à voir avec UTF-16. Par exemple, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")compilera correctement, mais échouera inconditionnellement: l'entrée sera traitée comme la chaîne UCS-2 u"\xD83C\xDF4C", qui ne peut pas être convertie en UTF-8 car UTF-8 ne peut coder aucune valeur dans la plage 0xD800-0xDFFF.

Toujours sur le front UCS-2, il n'y a aucun moyen de lire à partir d'un flux d'octets UTF-16 dans une chaîne UTF-16 avec ces facettes. Si vous avez une séquence d'octets UTF-16, vous ne pouvez pas la désérialiser en une chaîne de char16_t. C'est surprenant, car il s'agit plus ou moins d'une conversion d'identité. Encore plus surprenant, cependant, est le fait qu'il existe un support pour la désérialisation d'un flux UTF-16 dans une chaîne UCS-2 avec codecvt_utf16<char16_t>, qui est en fait une conversion avec perte.

Le support UTF-16-as-bytes est cependant assez bon: il prend en charge la détection de l'extrémité d'une nomenclature, ou sa sélection explicite dans le code. Il prend également en charge la production de sorties avec et sans nomenclature.

Il y a des possibilités de conversion plus intéressantes absentes. Il n'y a aucun moyen de désérialiser d'un flux ou d'une chaîne d'octets UTF-16 en une chaîne UTF-8, car UTF-8 n'est jamais pris en charge en tant que forme désérialisée.

Et ici, le monde étroit / large est complètement séparé du monde UTF / UCS. Il n'y a pas de conversion entre les encodages étroit / large à l'ancienne et les encodages Unicode.

Bibliothèque d'entrée / sortie

La bibliothèque d'E / S peut être utilisée pour lire et écrire du texte dans des encodages Unicode à l'aide des fonctions wstring_convertet wbuffer_convertdécrites ci-dessus. Je ne pense pas qu'il y ait grand-chose d'autre qui devrait être pris en charge par cette partie de la bibliothèque standard.

Bibliothèque d'expressions régulières

J'ai déjà exposé des problèmes avec les expressions régulières C ++ et Unicode sur Stack Overflow. Je ne répéterai pas tous ces points ici, mais simplement déclarer que les expressions rationnelles C ++ n'ont pas de support Unicode de niveau 1, ce qui est le strict minimum pour les rendre utilisables sans recourir à UTF-32 partout.

C'est tout?

Oui c'est ça. C'est la fonctionnalité existante. Il existe de nombreuses fonctionnalités Unicode qui sont introuvables, comme les algorithmes de normalisation ou de segmentation de texte.

U + 1F4A9 . Existe-t-il un moyen d'obtenir un meilleur support Unicode en C ++?

Les suspects habituels: ICU et Boost.Locale .


† Une chaîne d'octets est, sans surprise, une chaîne d'octets, c'est-à-dire des charobjets. Cependant, contrairement à une chaîne littérale large , qui est toujours un tableau d' wchar_tobjets, une "chaîne large" dans ce contexte n'est pas nécessairement une chaîne d' wchar_tobjets. En fait, la norme ne définit jamais explicitement ce que signifie une "chaîne large", donc il nous reste à deviner la signification de l'utilisation. Étant donné que la terminologie standard est bâclée et déroutante, j'utilise la mienne, au nom de la clarté.

Les codages comme UTF-16 peuvent être stockés sous forme de séquences de char16_t, qui n'ont alors aucune endianité; ou ils peuvent être stockés sous forme de séquences d'octets, qui ont une endianité (chaque paire consécutive d'octets peut représenter une char16_tvaleur différente selon l'endianness). La norme prend en charge ces deux formulaires. Une séquence de char16_test plus utile pour la manipulation interne dans le programme. Une séquence d'octets est le moyen d'échanger de telles chaînes avec le monde extérieur. Les termes que j'utiliserai à la place de "octet" et "large" sont donc "sérialisés" et "désérialisés".

‡ Si vous êtes sur le point de dire "mais Windows!" tenez votre 🐎🐎 . Toutes les versions de Windows depuis Windows 2000 utilisent UTF-16.

☦ Oui, je connais les großes Eszett (ẞ), mais même si vous deviez changer tous les paramètres régionaux allemands du jour au lendemain pour avoir ß majuscule en ẞ, il y a encore beaucoup d'autres cas où cela échouerait. Essayez de mettre U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ en majuscules. Il n'y a pas de ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; c'est juste des majuscules à deux F. Ou U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; il n'y a pas de capital pré-composé; il met juste des majuscules à un J majuscule et un caron combinant.

R. Martinho Fernandes
la source
26
Plus j'en lisais, plus j'avais le sentiment de ne rien comprendre à tout ça. J'ai lu la plupart de ces trucs il y a quelques mois et j'ai toujours l'impression de tout découvrir à nouveau ... Pour faire simple pour mon pauvre cerveau qui fait maintenant un peu mal, tous ces conseils sur utf8everywhere sont toujours valables, droite? Si je veux "juste" que mes utilisateurs puissent ouvrir et écrire des fichiers quels que soient leurs paramètres système, je peux leur demander le nom du fichier, le stocker dans une chaîne std :: string et tout devrait fonctionner correctement, même sous Windows? Désolé de demander (encore) ...
Uflex
5
@Uflex Tout ce que vous pouvez vraiment faire avec std :: string est de le traiter comme un blob binaire. Dans une implémentation Unicode correcte, ni l'interne (car il est caché profondément dans les détails de l'implémentation) ni l'encodage externe ne comptent (enfin, en quelque sorte, vous devez toujours avoir un encodeur / décodeur disponible).
Cat Plus Plus
3
@Uflex peut-être. Je ne sais pas si suivre des conseils que vous ne comprenez pas est une bonne idée.
R. Martinho Fernandes
1
Il existe une proposition de prise en charge d'Unicode dans C ++ 2014/17. Cependant, c'est dans 1, peut-être 4 ans et de peu d'utilité maintenant. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds
20
@ graham.reeds haha, merci, mais j'étais conscient de cela. Consultez la section "Remerciements";)
R. Martinho Fernandes
40

Unicode n'est pas pris en charge par la bibliothèque standard (pour toute signification raisonnable de pris en charge).

std::stringn'est pas meilleur que std::vector<char>: il est complètement inconscient d'Unicode (ou de toute autre représentation / encodage) et traite simplement son contenu comme une goutte d'octets.

Si vous avez seulement besoin de stocker et de caténer des blobs , cela fonctionne plutôt bien; mais dès que vous souhaitez une fonctionnalité Unicode (nombre de points de code , nombre de graphèmes, etc.), vous n'avez pas de chance.

La seule bibliothèque complète que je connaisse pour cela est ICU . L'interface C ++ est cependant dérivée de celle de Java, elle est donc loin d'être idiomatique.

Matthieu M.
la source
2
Que diriez - vous Boost.Locale ?
Uflex
11
@Uflex: depuis la page que vous avez liée Pour atteindre cet objectif, Boost.Locale utilise la bibliothèque Unicode et de localisation de pointe: ICU - International Components for Unicode.
Matthieu M.
1
Boost.Locale prend en charge d'autres backends non-ICU, voir ici: boost.org/doc/libs/1_53_0/libs/locale/doc/html/…
Superfly Jon
@SuperflyJon: C'est vrai, mais d'après cette même page, le support pour Unicode des backends non-ICU est "sévèrement limité".
Matthieu M.
24

Vous pouvez stocker en toute sécurité UTF-8 dans un std::string(ou dans un char[]ou char*, d'ailleurs), en raison du fait qu'un Unicode NUL (U + 0000) est un octet nul en UTF-8 et que c'est le seul moyen d'un nul byte peut apparaître en UTF-8. Par conséquent, vos chaînes UTF-8 seront correctement terminées selon toutes les fonctions de chaînes C et C ++, et vous pouvez les utiliser avec les iostreams C ++ (y compris std::coutet std::cerr, tant que votre langue est UTF-8).

Ce que vous ne pouvez pas faire avec std::stringpour UTF-8, c'est obtenir la longueur en points de code. std::string::size()vous indiquera la longueur de la chaîne en octets , qui est uniquement égale au nombre de points de code lorsque vous êtes dans le sous-ensemble ASCII de UTF-8.

Si vous avez besoin d'opérer sur des chaînes UTF-8 au niveau du point de code (c'est-à-dire pas seulement de les stocker et de les imprimer) ou si vous avez affaire à UTF-16, qui est susceptible d'avoir de nombreux octets nuls internes, vous devez examiner les types de chaînes de caractères larges.

Uckelman
la source
3
std::stringpeut être jeté dans iostreams avec des valeurs nulles intégrées très bien.
R. Martinho Fernandes le
3
C'est tout à fait prévu. Ça ne casse pas c_str()du tout parce que ça size()marche toujours. Seules les API cassées (c'est-à-dire celles qui ne peuvent pas gérer les nulls incorporés comme la plupart du monde C) se cassent.
R. Martinho Fernandes le
1
Les valeurs nulles incorporées se cassent c_str()car elles c_str()sont censées renvoyer les données sous la forme d'une chaîne C terminée par un zéro - ce qui est impossible, car les chaînes C ne peuvent pas avoir de valeurs nulles incorporées.
uckelman le
4
Plus maintenant. c_str()renvoie maintenant simplement le même que data(), c'est-à-dire tout. Les API qui prennent une taille peuvent la consommer. Les API qui ne le font pas, ne le peuvent pas.
R. Martinho Fernandes
6
Avec la légère différence qui c_str()garantit que le résultat est suivi d'un objet de type char NUL, et je ne pense pas que ce soit le data()cas. Non, on dirait que data()maintenant fait ça aussi. (Bien sûr, ce n'est pas nécessaire pour les API qui consomment la taille au lieu de l'inférer à partir d'une recherche de terminateur)
Ben Voigt
8

C ++ 11 a quelques nouveaux types de chaînes littérales pour Unicode.

Malheureusement, le support dans la bibliothèque standard pour les encodages non uniformes (comme UTF-8) est toujours mauvais. Par exemple, il n'y a pas de moyen agréable d'obtenir la longueur (en points de code) d'une chaîne UTF-8.

Un mec programmeur
la source
Alors devons-nous toujours utiliser std :: wstring pour les noms de fichiers si nous voulons prendre en charge les langues non latines? Parce que les nouveaux littéraux de chaîne n'aident pas vraiment ici car la chaîne vient généralement de l'utilisateur ...
Uflex
7
@Uflex std::stringpeut contenir une chaîne UTF-8 sans problème, mais par exemple la lengthméthode renvoie le nombre d'octets dans la chaîne et non le nombre de points de code.
Un mec programmeur le
8
Pour être honnête, obtenir la longueur en points de code d'une chaîne n'a pas beaucoup d'utilisations. La longueur en octets peut être utilisée pour pré-allouer correctement les tampons, par exemple.
R. Martinho Fernandes
2
Le nombre de points de code dans une chaîne UTF-8 n'est pas un nombre très intéressant: on peut écrire ñcomme 'LATIN MINUSCULE LETTRE N AVEC TILDE' (U + 00F1) (qui est un point de code) ou 'LATIN MINUSCULE N' ( U + 006E) suivi de 'COMBINING TILDE' (U + 0303) qui est deux points de code.
Martin Bonner soutient Monica le
Tous ces commentaires sur "vous n'avez pas besoin de ceci et vous n'avez pas besoin de ce" comme "nombre de points de code sans importance" etc. me semblent un peu louche. Une fois que vous avez écrit un analyseur qui est censé analyser le code source utf8, il appartient à la spécification de l'analyseur qu'il considère ou non LATIN SMALL LETTER N' == (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler
4

Cependant, il existe une bibliothèque assez utile appelée tiny-utf8 , qui est essentiellement un remplacement de std::string/ std::wstring. Il vise à combler le vide de la classe de conteneur utf8-string encore manquante.

C'est peut-être la manière la plus confortable de «traiter» les chaînes utf8 (c'est-à-dire sans normalisation unicode et autres choses similaires). Vous opérez confortablement sur les points de code , tandis que votre chaîne reste codée en s codés en longueur d'exécution char.

Jakob Riedle
la source