web-dev-qa-db-fra.com

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

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 l’Unicode?
  • Est-ce que std::string fait ce qu'il devrait?
  • Comment puis-je l'utiliser?
  • Où sont les problèmes potentiels?
173
Ralph Tandetzky

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

Terriblement.

Une analyse rapide des installations de la bibliothèque susceptibles de prendre en charge Unicode me donne cette liste:

  • Bibliothèque de cordes
  • 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.

Est-ce que std::string fait ce qu'il devrait?

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

Le modèle de classe basic_string décrit des objets pouvant stocker une séquence composée d'un nombre variable d'objets de type caractère arbitraires, le premier élément de la séquence étant à la position zéro.

Eh bien, std::string le fait très bien. Est-ce que cela fournit une fonctionnalité spécifique à Unicode? Non.

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

Comment puis-je l'utiliser?

Utilisez-le comme une séquence d'objets char; prétendre que c'est autre chose finira forcément par souffrir.

Où sont les problèmes potentiels?

Partout? Voyons voir...

Bibliothèque de chaînes de caractères

La bibliothèque de chaînes nous fournit basic_string, qui est simplement une séquence de ce que la norme appelle des "objets de type caractère". 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. Ceci est une vue de texte adaptée à la sérialisation/désérialisation/stockage.

Il fournit également des 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 et c32rtomb/mbrtoc32.

Bibliothèque de localisation

La bibliothèque de localisation croit toujours que l'un de ces "objets de type caractère" est égal à un "caractère". Ceci est bien sûr idiot et rend impossible le fonctionnement correct de beaucoup de choses au-delà d’un petit sous-ensemble de Unicode comme ASCII.

Considérons, par exemple, ce que la norme appelle "interfaces pratiques" dans l'en-tête <locale>:

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 espérez-vous que l'une ou l'autre 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 des paramètres régionaux appropriés si vous utilisiez char32_t uniquement: U'\U0001F34C' est une unité de code unique en UTF-32.

Cependant, cela signifie que vous n'obtenez que les simples transformations de casse avec toupper et tolower, qui, par exemple, ne sont pas suffisantes pour certaines langues allemandes: "ß" est remplacé par "SS" mais toupper ne peut en retourner qu'un personnage unité de code.

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 le standard appelle une chaîne d'octet et une chaîne large. Étant donné que ces termes sont vraiment trompeurs, je préfère utiliser "sérialisé" et "désérialisé", respectivement †.

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

wbuffer_convert remplit une fonction similaire mais en tant que large tampon de flux désérialisé qui enveloppe un octet tampon de flux sérialisé. Toute E/S est effectuée via le sous-jacent octet tampon de flux sérialisé avec conversions vers et depuis les codages 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 se désérialise.

La norme fournit des modèles de classe de codecvt à utiliser avec les fonctionnalités suivantes: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16 et certaines spécialisations codecvt. Ensemble, ces facettes standard fournissent toutes les conversions suivantes. (Remarque: dans la liste suivante, le codage à gauche correspond toujours à la chaîne/streambuf sérialisée et le codage à droite correspond toujours à la chaîne/streambuf désérialisée; la norme 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-8 ↔ UTF-32 avec 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-8 ↔ UTF-16 avec 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 délicates ici.

Tout d'abord, sainte haute mère porteuse! ce schéma de nommage est en désordre.

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

Je dirais que char16_t est évidemment destiné à stocker des unités de code UTF-16. Cependant, ceci 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 bien, 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 comprise dans la plage 0xD800-0xDFFF.

Toujours sur le front UCS-2, il n’existe aucun moyen de lire 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. Ceci est surprenant, car il s’agit plus ou moins d’une conversion d’identité. Ce qui est encore plus surprenant, toutefois, est le fait que la désérialisation d’un flux UTF-16 vers une chaîne UCS-2 avec codecvt_utf16<char16_t>, qui est en réalité une conversion avec perte, est prise en charge.

La prise en charge UTF-16-as-octets est tout à fait satisfaisante: elle prend en charge la détection d’endianess à partir d’une nomenclature, ou sa sélection explicite dans le code. Il prend également en charge la production d'une sortie avec et sans nomenclature.

Certaines possibilités de conversion plus intéressantes sont absentes. Il n'est pas possible de désérialiser un flux d'octets ou une chaîne UTF-16 en une chaîne UTF-8, car UTF-8 n'est jamais pris en charge en tant que formulaire désérialisé.

Et ici, le monde étroit/large est complètement séparé du monde UTF/UCS. Il n'y a pas de conversions entre les codages étroit/large de style ancien et les codages Unicode.

Bibliothèque d'entrées/sorties

La bibliothèque d'E/S peut être utilisée pour lire et écrire du texte au codage Unicode à l'aide des fonctions wstring_convert et wbuffer_convert décrites ci-dessus. Je ne pense pas qu'il y ait beaucoup d'autres choses qui devraient être supportées par cette partie de la bibliothèque standard.

Bibliothèque d'expressions régulières

J'ai exposé sur des problèmes avec regexes C++ et Unicode sur Stack Overflow avant. Je ne vais pas répéter 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 minimum nécessaire pour les rendre utilisables sans avoir recours à l’utilisation de UTF-32 partout.

C'est ça?

Oui c'est ça. C'est la fonctionnalité existante. Il y a beaucoup de fonctionnalités Unicode qui ne sont nulle part ailleurs, comme les algorithmes de normalisation ou de segmentation de texte.

+ 1F4A9 . Est-il possible 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, char objets. Cependant, contrairement à un littéral de chaîne large , qui est toujours un tableau d'objets wchar_t, une "chaîne large" dans ce contexte n'est pas nécessairement une chaîne de wchar_t objets. En fait, la norme ne définit jamais explicitement ce que signifie "chaîne large", nous devons donc deviner le sens de l'utilisation. Comme 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 finalité. ou ils peuvent être stockés sous forme de séquences d'octets, qui ont une finalité (chaque paire d'octets consécutive peut représenter une valeur différente de char16_t en fonction de la finalité). La norme prend en charge ces deux formes. Une séquence de char16_t est plus utile pour une 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 au lieu 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 toutes les locales allemandes du jour au lendemain pour que ß soit en majuscule, il y a encore beaucoup d'autres cas où cela échouerait. Essayez de mettre en majuscule U + FB00 sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Il n'y a pas de ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; il se limite à deux Fs. Ou U + 01F0 sᴍᴀʟʟ ᴡɪᴛʜ; il n'y a pas de capital précomposé; il ne fait que mettre en majuscule un J majuscule et un caron combinant.

256

Unicode n'est pas pris en charge par Standard Library (pour toute signification raisonnable de pris en charge).

std::string n'est pas meilleur que std::vector<char>: il ignore complètement Unicode (ou toute autre représentation/encodage) et traite simplement son contenu comme un blob 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 graphemes 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 l'interface Java, elle est donc loin d'être idiomatique.

38
Matthieu M.

Vous pouvez stocker en toute sécurité UTF-8 dans un std::string (ou dans un char[] ou char*, d'ailleurs), car un Unicode NUL (U + 0000) est un octet nul en UTF-8 et qu’il s’agit de la seule façon pour un octet nul de se produire en UTF-8. Par conséquent, vos chaînes UTF-8 seront correctement terminées en fonction de toutes les fonctions de chaîne C et C++, et vous pouvez les utiliser avec iostreams C++ (y compris std::cout et std::cerr, aussi longtemps que votre environnement local est UTF-8).

Ce que vous ne pouvez pas faire avec std::string pour UTF-8 est d’obtenir la longueur en points de code. std::string::size() vous indiquera la longueur de la chaîne en octets , ce qui n’est égal qu’au nombre de points de code lorsque vous vous trouvez dans le ASCII sous-ensemble de UTF-8.

Si vous devez utiliser des chaînes UTF-8 au niveau point de code (c’est-à-dire ne pas simplement les stocker et les imprimer) ou Si vous utilisez UTF-16, qui est susceptible de comporter de nombreux octets nuls internes, vous devez vous pencher sur les types de chaînes de caractères larges.

23
uckelman

C++ 11 a un couple de 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’existe aucun moyen intéressant d’obtenir la longueur (en points de code) d’une chaîne UTF-8.

8

Cependant, il existe une bibliothèque très utile appelée tiny-utf8 , qui est fondamentalement un remplacement immédiat pour std::string/std::wstring. Son objectif est de combler le vide de la classe de conteneur utf8-string qui manque encore.

C'est peut-être le moyen le plus pratique de "traiter" avec les chaînes utf8 (c'est-à-dire sans normalisation unicode et autres choses similaires). Vous opérez facilement sur points de code , tandis que votre chaîne reste encodée en chars.

3
Jakob Riedle