60 votes

WChars, encodages, normes et portabilité

Ce qui suit n'est peut-être pas une question sur les SO ; si cela dépasse les limites, n'hésitez pas à me dire de m'en aller. La question est la suivante : "Est-ce que je comprends correctement la norme C et est-ce que c'est la bonne façon de procéder ?".

J'aimerais demander des éclaircissements, des confirmations et des corrections sur ma compréhension de la gestion des caractères en C (et donc en C++ et C++0x). Tout d'abord, une observation importante :

La portabilité et la sérialisation sont des concepts orthogonaux.

Les choses portables sont des choses comme le C, unsigned int , wchar_t . Les choses sérialisables sont des choses comme uint32_t ou UTF-8. "Portable" signifie que vous pouvez recompiler la même source et obtenir un résultat fonctionnel sur toutes les plateformes supportées, mais la représentation binaire peut être totalement différente (ou même ne pas exister, par exemple le pigeon TCP sur porteur). Les choses sérialisables, d'un autre côté, ont toujours l'aspect mismo représentation, par exemple le fichier PNG que je peux lire sur mon bureau Windows, sur mon téléphone ou sur ma brosse à dents. Les objets portables sont internes, les objets sérialisables s'occupent des E/S. Les choses portables sont typesafe, les choses sérialisables ont besoin de type punning. </preamble>

Lorsqu'il s'agit de la gestion des caractères en C, il existe deux groupes de choses liées respectivement à la portabilité et à la sérialisation :

  • wchar_t , setlocale() , mbsrtowcs() / wcsrtombs() : Le standard C ne dit rien sur les "encodages". En fait, il est entièrement indépendant des propriétés du texte ou de l'encodage. Il dit seulement "votre point d'entrée est main(int, char**) ; vous obtenez un type wchar_t qui peut contenir tous les caractères de votre système ; vous obtenez des fonctions pour lire les séquences de caractères en entrée et les transformer en chaînes de caractères exploitables et vice versa.

  • iconv() et UTF-8,16,32 : Une fonction/bibliothèque pour transcoder entre des encodages bien définis, précis et fixes. Tous les encodages gérés par iconv sont universellement compris et acceptés, à une exception près.

Le pont entre le monde portable et agnostique de l'encodage du C et son système de gestion de l'encodage. wchar_t type de caractère portable et le monde extérieur déterministe est Conversion iconv entre WCHAR-T et UTF .

Donc, dois-je toujours stocker mes chaînes de caractères en interne dans un wstring agnostique, et m'interfacer avec le CRT par l'intermédiaire de wcsrtombs() et utiliser iconv() pour la sérialisation ? Conceptuellement :

                        my program
    <-- wcstombs ---  /==============\   --- iconv(UTF8, WCHAR_T) -->
CRT                   |   wchar_t[]  |                                <Disk>
    --- mbstowcs -->  \==============/   <-- iconv(WCHAR_T, UTF8) ---
                            |
                            +-- iconv(WCHAR_T, UCS-4) --+
                                                        |
       ... <--- (adv. Unicode malarkey) ----- libicu ---+

En pratique, cela signifie que j'écrirais deux wrappers de type boiler-plate pour le point d'entrée de mon programme, par exemple pour C++ :

// Portable wmain()-wrapper
#include <clocale>
#include <cwchar>
#include <string>
#include <vector>

std::vector<std::wstring> parse(int argc, char * argv[]); // use mbsrtowcs etc

int wmain(const std::vector<std::wstring> args); // user starts here

#if defined(_WIN32) || defined(WIN32)
#include <windows.h>
extern "C" int main()
{
  setlocale(LC_CTYPE, "");
  int argc;
  wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc);
  return wmain(std::vector<std::wstring>(argv, argv + argc));
}
#else
extern "C" int main(int argc, char * argv[])
{
  setlocale(LC_CTYPE, "");
  return wmain(parse(argc, argv));
}
#endif
// Serialization utilities

#include <iconv.h>

typedef std::basic_string<uint16_t> U16String;
typedef std::basic_string<uint32_t> U32String;

U16String toUTF16(std::wstring s);
U32String toUTF32(std::wstring s);

/* ... */

Est-ce la bonne façon d'écrire un noyau de programme idiomatique, portable, universel, agnostique en matière d'encodage, en utilisant uniquement du C/C++ standard pur, ainsi qu'une interface E/S bien définie vers l'UTF en utilisant iconv ? (Notez que des questions telles que la normalisation Unicode ou le remplacement des diacritiques sont en dehors du champ d'application ; ce n'est qu'après avoir décidé que vous voulez réellement Unicode (par opposition à tout autre système de codage qui vous plairait) est-il temps de s'occuper de ces spécificités, par exemple en utilisant une bibliothèque dédiée comme libicu).

Mises à jour

Suite à de nombreux commentaires très sympathiques, j'aimerais ajouter quelques observations :

  • Si votre application souhaite explicitement traiter du texte Unicode, vous devez faire de l'option iconv -conversion partie du noyau et utilisation uint32_t / char32_t -les chaînes de caractères en interne avec UCS-4.

  • Les fenêtres : Bien que l'utilisation de chaînes de caractères larges soit généralement satisfaisante, il semble que l'interaction avec la console (n'importe quelle console, d'ailleurs) soit limitée, car il ne semble pas y avoir de support pour un encodage multi-octets raisonnable de la console et mbstowcs est essentiellement inutile (sauf pour un élargissement trivial). Recevoir des arguments à chaîne large, par exemple d'une chute d'explorateur avec GetCommandLineW + CommandLineToArgvW fonctionne (peut-être devrait-il y avoir un wrapper séparé pour Windows).

  • Les systèmes de fichiers : Les systèmes de fichiers ne semblent pas avoir de notion de codage et prennent simplement n'importe quelle chaîne à terminaison nulle comme nom de fichier. La plupart des systèmes prennent des chaînes d'octets, mais Windows/NTFS prend des chaînes de 16 bits. Vous devez faire attention lorsque vous découvrez quels fichiers existent et lorsque vous manipulez ces données (par ex. char16_t les séquences qui ne constituent pas des UTF16 valides (par exemple, les substituts nus) sont des noms de fichiers NTFS valides). La norme C fopen n'est pas en mesure d'ouvrir tous les fichiers NTFS, puisqu'il n'existe aucune conversion possible qui correspondrait à toutes les chaînes de caractères 16 bits possibles. L'utilisation de l'option spécifique à Windows _wfopen peuvent être nécessaires. En corollaire, il n'existe en général aucune notion bien définie du "nombre de caractères" d'un nom de fichier donné, puisqu'il n'existe aucune notion de "caractère" en premier lieu. Caveat emptor.

21voto

Philipp Points 21479

Est-ce la bonne façon d'écrire un noyau de programme idiomatique, portable, universel et agnostique en matière d'encodage en utilisant uniquement du C/C++ standard pur ?

Non, et il n'existe aucun moyen de remplir toutes ces propriétés, du moins si vous voulez que votre programme fonctionne sous Windows. Sous Windows, vous devez ignorer les standards C et C++ presque partout et travailler exclusivement avec wchar_t (pas nécessairement en interne, mais à toutes les interfaces du système). Par exemple, si vous commencez avec

int main(int argc, char** argv)

vous avez déjà perdu le support Unicode pour les arguments de la ligne de commande. Vous devez écrire

int wmain(int argc, wchar_t** argv)

à la place, ou utiliser l'option GetCommandLineW dont aucune n'est spécifiée dans la norme C.

Plus précisément,

  • tout programme compatible avec Unicode sous Windows doit activement ignorer la norme C et C++ pour des choses comme les arguments de la ligne de commande, les entrées/sorties de fichiers et de console, ou la manipulation de fichiers et de répertoires. Ce n'est certainement pas idiomatique . Utilisez plutôt les extensions Microsoft ou des wrappers comme Boost.Filesystem ou Qt.
  • Portabilité est extrêmement difficile à réaliser, notamment pour la prise en charge d'Unicode. Il faut vraiment se préparer à ce que tout ce que l'on croit savoir puisse être faux. Par exemple, vous devez tenir compte du fait que les noms de fichiers que vous utilisez pour ouvrir des fichiers peuvent être différents des noms de fichiers réellement utilisés, et que deux noms de fichiers apparemment différents peuvent représenter le même fichier. Après avoir créé deux fichiers a y b vous pourriez vous retrouver avec un seul fichier c ou deux fichiers d y e dont les noms de fichiers sont différents des noms de fichiers que vous avez transmis au système d'exploitation. Soit vous avez besoin d'une bibliothèque externe, soit vous avez besoin d'un grand nombre de fichiers de type #ifdef s.
  • Codage de l'agnosticité ne fonctionne généralement pas dans la pratique, surtout si l'on veut être portable. Vous devez savoir que wchar_t est une unité de code UTF-16 sous Windows et que char est souvent (mais pas toujours) une unité de code UTF-8 sous Linux. La connaissance de l'encodage est souvent l'objectif le plus souhaitable : assurez-vous de toujours savoir avec quel encodage vous travaillez, ou utilisez une bibliothèque qui les rend abstraits.

Je pense que je dois conclure qu'il est totalement impossible de construire une application portable compatible avec Unicode en C ou C++, à moins que vous ne soyez prêt à utiliser des bibliothèques supplémentaires et des extensions spécifiques au système, et à y consacrer beaucoup d'efforts. Malheureusement, la plupart des applications échouent déjà à des tâches relativement simples comme "écrire des caractères grecs sur la console" ou "supporter correctement tout nom de fichier autorisé par le système", et ces tâches ne sont que les premiers petits pas vers un véritable support Unicode.

9voto

dan04 Points 33306

J'éviterais le wchar_t parce qu'il est dépendant de la plate-forme (pas "sérialisable" selon votre définition) : UTF-16 sous Windows et UTF-32 sur la plupart des systèmes de type Unix. Au lieu de cela, utilisez l'option char16_t et/ou char32_t de C++0x/C1x (si vous n'avez pas de nouveau compilateur, définissez-les en tant que uint16_t y uint32_t pour le moment).

DO définir des fonctions de conversion entre les fonctions UTF-8, UTF-16 et UTF-32.

N'ESSAYEZ PAS écrire des versions étroites et larges surchargées de chaque comme l'API Windows l'a fait avec -A et -W. Choisissez un l'encodage préféré à utiliser en interne, et s'y tenir. Pour les choses qui nécessitent un encodage différent, convertissez-le si nécessaire.

8voto

Dietrich Epp Points 72865

Le problème avec wchar_t est que le traitement de texte agnostique est trop difficile et doit être évité. Si vous vous en tenez au "C pur", comme vous le dites, vous pouvez utiliser tous les outils de traitement du texte de l w* des fonctions comme wcscat et ses amis, mais si vous voulez faire quelque chose de plus sophistiqué, vous devez plonger dans l'abîme.

Voici des choses qui sont beaucoup plus difficiles avec wchar_t qu'ils ne le sont si vous choisissez simplement l'un des encodages UTF :

  • Parsing Javascript : Les identificateurs peuvent contenir certains caractères en dehors du BMP (et supposons que vous vous souciez de ce type d'exactitude).

  • HTML : Comment transforme-t-on &#65536; en une chaîne de wchar_t ?

  • Éditeur de texte : Comment trouver les limites de groupes de graphèmes dans une wchar_t chaîne ?

Si je connais l'encodage d'une chaîne, je peux examiner les caractères directement. Si je ne connais pas l'encodage, je dois espérer que ce que je veux faire avec une chaîne est implémenté par une fonction de bibliothèque quelque part. Ainsi, la portabilité de wchar_t est quelque peu hors de propos, car je ne considère pas qu'il s'agisse d'un problème particulièrement grave. utile type de données.

Les exigences de votre programme peuvent être différentes et wchar_t peut fonctionner correctement pour vous.

6voto

Luc Danton Points 21421

Étant donné que iconv n'est pas un "pur standard C/C++", je ne pense pas que vous répondiez à vos propres spécifications.

Il y a de nouvelles codecvt facettes venant avec char32_t y char16_t Je ne vois donc pas comment vous pouvez vous tromper tant que vous êtes cohérent et que vous choisissez un seul type de caractères + encodage si les facettes sont là.

Les facettes sont décrites dans 22.5 [locale.stdcvt] (de n3242).


Je ne comprends pas comment cela ne satisfait pas au moins une partie de vos exigences :

namespace ns {

typedef char32_t char_t;
using std::u32string;

// or use user-defined literal
#define LIT u32

// Communicate with interface0, which wants utf-8

// This type doesn't need to be public at all; I just refactored it.
typedef std::wstring_convert<std::codecvt_utf8<char_T>, char_T> converter0;

inline std::string
to_interface0(string const& s)
{
    return converter0().to_bytes(s);
}

inline string
from_interface0(std::string const& s)
{
    return converter0().from_bytes(s);
}

// Communitate with interface1, which wants utf-16

// Doesn't have to be public either
typedef std::wstring_convert<std::codecvt_utf16<char_T>, char_T> converter1;

inline std::wstring
to_interface0(string const& s)
{
    return converter1().to_bytes(s);
}

inline string
from_interface0(std::wstring const& s)
{
    return converter1().from_bytes(s);
}

} // ns

Votre code peut alors utiliser ns::string , ns::char_t , LIT'A' & LIT"Hello, World!" avec un abandon insouciant, sans savoir quelle est la représentation sous-jacente. Ensuite, utilisez from_interfaceX(some_string) chaque fois que cela est nécessaire. Cela n'affecte pas non plus la locale globale ou les flux. Les aides peuvent être aussi astucieuses que nécessaire, par ex. codecvt_utf8 peut s'occuper des "en-têtes", ce qui, je suppose, est le Standardese de trucs délicats comme la nomenclature (idem codecvt_utf16 ).

En fait, j'ai écrit ce qui précède pour être aussi bref que possible, mais vous avez vraiment besoin d'aides comme celle-ci :

template<typename... T>
inline ns::string
ns::from_interface0(T&&... t)
{
    return converter0().from_bytes(std::forward<T>(t)...);
}

qui vous donnent accès aux 3 surcharges pour chaque [from|to]_bytes en acceptant des choses comme par exemple const char* ou des gammes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X