C++ framework for multimedia software like Games
Wed Nov 09 18:42:31 CET 2011
nb comments: 0

Après avoir fait 2 news sur le sujet des strings, je me dit autant parler des Unicode.
Il s'agit bien souvent d'un sujet très mal connue et épineux pour les développeurs.

A défaut de trouver une librairie satisfaisante et également à cause de ma volonté de faire une lib des plus light et ayant le moins de dépendances possible, j'ai recodé un petit système d'Unicode string en héritant simplement de std::basic_string. Celui-ci me permet alors de gérer correctement et simplement les Unicode et les conversions avec d'autres formats tel que l'ascii.

Tout d'abord qu'est ce que l'unicode ?

La réponse au multiples langages Caractère/hiéroglyphe/Symbole présent sur terre.
D’après Wikipedia, il s'agit donc "d'une norme informatique permettant de donner a tout caractère de n'importe quel système d’écriture un identifiant numérique". Tout comme la table ascii qui permet de coder l'alphabet anglais (et français avec la table étendue.)  L'ascii est une norme de l'ANSI et on assimilera souvent l'ANSI pour de l'ascii.

Chaque caractère de la table ascii est codé sur un octet. Il est donc impossible de représenter plus de 256 caractère avec cette norme. Et du coup il devient impossible d'implanter l'alphabet chinois, japonais, russe, etc...

Le Consortium Unicode nous a donc proposé une norme Unicode. Celle ci est une norme international et est largement reconnu et utilisé dans le monde entier.
L'Unicode accepte plusieurs format de représentation :
  • UTF-8      -> Utilisé un peu partout notamment sur internet, utilise un codage de taille variable sur 8 bit permettant d’être un minimum coûteux en terme de mémoire. Et du coup plus lent au traitement
  • UTF-16    -> Utilise un codage à taille variable sur 16 bit, cependant ce format permet de représenter une grande majorité de caractères modernes. Stockage plus lourd en mémoire mais avec un traitement un peu plus rapide.
  • UTF-32    -> Utilise un codage à taille fixe. Traitement beaucoup plus simple mais avec beaucoup de mémoire perdu pour rien.

Gestion de l'UTF-32 :

Pour 3dNovac j'ai choisi de ne gérer que des caractères Unicode code en UTF-32. Ceci principalement pour des raisons de performances et de simplicités.

Premièrement je gère ceci avec une classe héritant de basic_string<UInt32>, et avec des fonctions de conversions de base.
L’héritage de basic_string nous permet de conserver un grande compatibilité avec les std::string.

class UTF32 : public std::basic_string<Nc::UInt32>
{
    public:
        UTF32()  {}
        UTF32(const char *str);
        UTF32(const wchar_t *str);
        UTF32(const Nc::UInt32 *str);
        UTF32(const std::string &str);
        UTF32(const std::wstring &str);
        virtual ~UTF32()    {}

        /** \return the number of occurence of the caractere 'c' in the unicode string */
        unsigned int        CharCount(Nc::UInt32 c) const;

        /** Convert and return the unicode string in standard std::string */
        const std::string   &ToStdString() const;

        /** Convert and return the unicode string in standard std::wstring */
        const std::wstring  &ToStdWString() const;

        friend std::ostream& operator << (std::ostream& os, const UTF32& s)
        {
                os << s.ToStdString();
                return os;
        }
};

Utilisation des locales pour effectuer les conversions :

Qu'est ce qu'une locale ?
 
En C++ il s'agit d'une classes inclue dans la std permettant de gérer la localisation des programmes et de réagir en fonction. Une locale correspondant donc a une certaine localisation. Par exemple si vous êtes sous Linux, vous pouvez afficher toutes les locales disponibles en exécutant "locale -a" sur la console.
Chaque locale dispose d'une liste de règles appelé facettes. Ces facettes permettent de définir différents comportements. Les facettes sont regroupé en 6 catégories différentes. Il est tout à fait possible de créer et d'ajouter ses propres facettes à une locale.
 
Voici donc les 6 différentes locales:
  • ctype qui regroupe toutes les facettes permettant la classification et la conversion de caractères.
  • collate qui permet la comparaison de caractères.
  • numeric qui comprend chaque facettes prenant en compte le formatage de nombres.
  • monetary qui comprend chaque facettes permettant de représenter les symboles monétaire ainsi que la manière d'afficher les montants.
  • time qui comprend les facettes s'occupant du formatage des dates et heures.
  • message qui permet de gérer l'internationalisation des programmes en traduisant chaque message dans la bonne langue.
 
Si cela vous intéresse voici un lien expliquant de long en large l'utilisation des locales : http://cpp.developpez.com/cours/cpp/?page=page_19

Ici on s’intéressera surtout aux facettes ctype qui vont nous permettre de convertir nos caractères d'une format a un autre. Ici de l'ANSI a l'UTF-32 et inversement.

Conversion ANSI to UTF-32 :

La conversion ANSI to UTF est très simple, on utilise directement la fonction widen de la facettes de conversion de caractère (la fonction narrow sera alors utilisé pour effectuer la conversion inverse). La std nous mâche tout le travail !
On utilise des wchar_t pour la conversion pour la simple raison que la table ascii est tellement restreinte que l'on peut se permettre une conversion UTF-16 (en principe le wchar_t correspond a de l'UTF-16).

void ANSIToUTF32(char *begin, char *end, UInt32 output)
{
    // Récupère la facette de la local qui va nous permettre une conversion
    // on prend la facette de type wchar_t correspondant au type recherche
    std::locale lc = std::locale();
    const std::ctype<wchar_t>& facet = std::use_facet< std::ctype<wchar_t> >(lc);

    // effectue la conversion en parcourant chaque caractère
    while (begin < end)
    {
        *output++ = static_cast<UInt32>(facet.widen(*begin++));
    }
}


Conversion UTF-16 to UTF-32 :

Pour la conversion UTF-16 to UTF-32, c'est un peut plus compliqué. La raison est toute simple, comme je l'ai dit plus haut, en UTF-16 les caractères n'ont pas une taille fixe ! On aura au maximum un codage sur 4 octets soit 2 * 16 bit.
Tout d'abord il faudra identifier sur quel type de codage on ce trouve, dans le cas de 2 octets on peut effectuer la conversion avec un simple cast, et dans le cas de 4 octets, nos caractères seront comprit entre 0xD800 et 0xDBFF, pour une explication voir : http://fr.wikipedia.org/wiki/UTF-16
Enfin en cas d'erreur (par exemple un décalage dans le codage) on utilisera un caractère de remplacement.

Attention ici on suppose que la machine est en litle endian.

void UTF16ToUTF32(Nc::UInt16 begin, Nc::UInt16 end, UInt32 output, UInt32 replacement)
{
    for (Nc::UInt16 c = *begin++; begin < end; )

    {
        //  4 octets ?
        if ((c >= 0xD800) && (c <= 0xDBFF))
        {
            if (begin < end)
            {
                Nc::UInt16 d = *begin++;
                // second character valid ?
             
  if ((d >= 0xDC00) && (d <= 0xDFFF))

                     *output++ = static_cast<Nc::UInt32>(((c - 0xD800) << 10) + (d - 0xDC00) + 0x0010000);
                else if (replacement)
                     *output++ = replacement;
              }
         }
         // invalid character
         
else if ((c >= 0xDC00) && (c <= 0xDFFF))

         {
             if (replacement)
                 *output++ = replacement;
         }
         // 2 octets ?c
         else
             *output++ = static_cast<Nc::UInt32>(c);
     }

}

L'encodage en C++ :

Voila maintenant que l'on sait convertir une chaîne de caractère d'un format à un autre ce serait bien de savoir dans quel encodage sont codée nos chaînes. Je m'explique, on sait déjà que les char* en C sont codée avec la norme ANSI (ascii) et cela ne devrai jamais changé étant donné que la norme C nous assure que sur n'importe que architecture, le char sera toujours la plus petite unité (sur 8 bit). Mais cela n'est pas forcement vrai dans le cas des wchar_t, ces caractères peuvent changer de taille d'une archi a l'autre en générale on aura du 16 bit mais ou peut ce retrouver avec certaine archi ou compilateur avec du 32 bit. Auquel cas on aura pas le même encodage.

Tableau récapitulatif :

 Type Size Encodage
 char 8 bit ANSI
wchar_t 16 bit UTF-16
wchar_t 32 bit UTF-32

Donc dans le cas du wchar_t on devra fait quelque chose comme ceci:

UTF32::UTF32(const wchar_t *str)
{
    size_t len = Strlen(str);
    if (len > 0)
    {
        reserve(len + 1);
        if (sizeof(wchar_t) == 2)
               Convert::Unicode::UTF16ToUTF32(str, str + len, std::back_inserter(*this), 0);
        else
            std::copy(str, str + len, std::back_inserter(*this));
        else
            throw Utils::Exception("UTF-32", "Error, Can't convert the wchar_t into an UTF-32 format.");
    }
}

la classe UTF-32 hérite de std::basic_string on utilise donc *this pour effectuer la conversion.

Je vous l'accorde c'est quelque chose de très chiant à faire et surtout à bien comprendre, mais au final ça na rien de si compliqué :)

Voila, cette fois ci j'en ai réellement fini avec les chaînes de caractères...
Et et un petit conseil, par pitié si un jour vous devez redéfinir votre propres classes de string, faites un héritage sur std::basic_string de sorte à avoir le même comportement et même fonctions que nos chère std::string que tout le monde sait a peu près utiliser.

Author: Ponpon
Comments
Pseudo
Add new comment:
Post
Loading...