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::string
ce qu'il doit faire? - Comment l'utiliser?
- Où sont les problèmes potentiels?
Réponses:
Terriblement.
Une analyse rapide des fonctionnalités de la bibliothèque susceptibles de fournir un support Unicode me donne cette liste:
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.
Oui. Selon la norme C ++, voici ce que
std::string
ses frères et sœurs devraient faire:Eh bien,
std::string
ça va très bien. Cela fournit-il des fonctionnalités spécifiques à Unicode? Non.Devrait-il? Probablement pas.
std::string
est bien comme une séquence d'char
objets. 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.Utilisez-le comme une séquence d'
char
objets; prétendre que c'est quelque chose d'autre ne peut que se terminer par la douleur.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
/mbrtoc16
etc32rtomb
/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:Comment vous attendez-vous à ce que l'une de ces fonctions catégorise correctement, par exemple, U + 1F34C ʙᴀɴᴀɴᴀ, comme dans
u8"🍌"
ouu8"\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_t
que: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
toupper
ettolower
, qui, par exemple, ne sont pas assez bonnes pour certains paramètres régionaux allemands: "ß" majuscules en "SS" ☦ maistoupper
ne peut renvoyer qu'une unité de code decaractère.Ensuite,
wstring_convert
/wbuffer_convert
et les facettes de conversion de code standard.wstring_convert
est 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_convert
exécute une fonction similaire, mais en tant que tampon de flux désérialisélargequi enveloppe un tampon de flux sérialisé d'octets. Toutes les E / S sont effectuées via le tampon de flux sérialisé d'octetssous-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 certainescodecvt
spé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).codecvt_utf8<char16_t>
etcodecvt_utf8<wchar_t>
oùsizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
,codecvt<char32_t, char, mbstate_t>
etcodecvt_utf8<wchar_t>
oùsizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
etcodecvt_utf16<wchar_t>
oùsizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
etcodecvt_utf16<wchar_t>
oùsizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
,codecvt<char16_t, char, mbstate_t>
etcodecvt_utf8_utf16<wchar_t>
oùsizeof(wchar_t) == 2
;codecvt<wchar_t, char_t, mbstate_t>
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_t
c'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-2u"\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 aveccodecvt_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_convert
etwbuffer_convert
dé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.
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.
Les suspects habituels: ICU et Boost.Locale .
† Une chaîne d'octets est, sans surprise, une chaîne d'octets, c'est-à-dire des
char
objets. Cependant, contrairement à une chaîne littérale large , qui est toujours un tableau d'wchar_t
objets, une "chaîne large" dans ce contexte n'est pas nécessairement une chaîne d'wchar_t
objets. 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 unechar16_t
valeur différente selon l'endianness). La norme prend en charge ces deux formulaires. Une séquence dechar16_t
est 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.
la source
Unicode n'est pas pris en charge par la bibliothèque standard (pour toute signification raisonnable de pris en charge).
std::string
n'est pas meilleur questd::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.
la source
Vous pouvez stocker en toute sécurité UTF-8 dans un
std::string
(ou dans unchar[]
ouchar*
, 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 comprisstd::cout
etstd::cerr
, tant que votre langue est UTF-8).Ce que vous ne pouvez pas faire avec
std::string
pour 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.
la source
std::string
peut être jeté dans iostreams avec des valeurs nulles intégrées très bien.c_str()
du tout parce que çasize()
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.c_str()
car ellesc_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.c_str()
renvoie maintenant simplement le même quedata()
, c'est-à-dire tout. Les API qui prennent une taille peuvent la consommer. Les API qui ne le font pas, ne le peuvent pas.c_str()
garantit que le résultat est suivi d'un objet de type char NUL, et je ne pense pas que ce soit ledata()
cas. Non, on dirait quedata()
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)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.
la source
std::string
peut contenir une chaîne UTF-8 sans problème, mais par exemple lalength
méthode renvoie le nombre d'octets dans la chaîne et non le nombre de points de code.ñ
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.LATIN SMALL LETTER N'
==(U+006E) followed by 'COMBINING TILDE' (U+0303)
.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
.la source