web-dev-qa-db-fra.com

Quel est l'intérêt des traits de caractère STL?

Je remarque que dans ma copie de la référence SGI STL, il y a une page sur les traits de caractère mais je ne vois pas comment ils sont utilisés? Remplacent-ils les fonctions string.h? Ils ne semblent pas être utilisés par std::string, Par exemple la méthode length() sur std::string n'utilise pas la méthode Traits de caractère length(). Pourquoi les traits de caractère existent-ils et sont-ils jamais utilisés dans la pratique?

79
Matthew Smith

Les traits de caractère sont un composant extrêmement important des bibliothèques de flux et de chaînes car ils permettent aux classes de flux/chaîne de séparer la logique de quels caractères sont stockés de la logique de quoi des manipulations doivent être effectuées sur ces personnages.

Pour commencer, la classe de traits de caractère par défaut, char_traits<T>, est largement utilisé dans la norme C++. Par exemple, il n'y a pas de classe appelée std::string. Il existe plutôt un modèle de classe std::basic_string qui ressemble à ceci:

template <typename charT, typename traits = char_traits<charT> >
    class basic_string;

Ensuite, std::string est défini comme

typedef basic_string<char> string;

De même, les flux standard sont définis comme

template <typename charT, typename traits = char_traits<charT> >
    class basic_istream;

typedef basic_istream<char> istream;

Alors pourquoi ces classes sont-elles structurées telles quelles? Pourquoi devrions-nous utiliser une classe de traits étranges comme argument de modèle?

La raison en est que dans certains cas, nous pourrions vouloir avoir une chaîne comme std::string, mais avec des propriétés légèrement différentes. Un exemple classique de ceci est si vous souhaitez stocker des chaînes d'une manière qui ignore la casse. Par exemple, je pourrais vouloir créer une chaîne appelée CaseInsensitiveString telle que je puisse avoir

CaseInsensitiveString c1 = "HI!", c2 = "hi!";
if (c1 == c2) {  // Always true
    cout << "Strings are equal." << endl;
}

Autrement dit, je peux avoir une chaîne où deux chaînes différant uniquement dans leur sensibilité à la casse sont comparées égales.

Supposons maintenant que les auteurs de bibliothèque standard aient conçu des chaînes sans utiliser de traits. Cela signifierait que j'aurais dans la bibliothèque standard une classe de cordes extrêmement puissante qui était entièrement inutile dans ma situation. Je ne pouvais pas réutiliser une grande partie du code pour cette classe de chaînes, car les comparaisons fonctionneraient toujours par rapport à la façon dont je voulais qu'elles fonctionnent. Mais en utilisant des traits, il est en fait possible de réutiliser le code qui pilote std::string pour obtenir une chaîne insensible à la casse.

Si vous extrayez une copie de la norme ISO C++ et regardez la définition du fonctionnement des opérateurs de comparaison de la chaîne, vous verrez qu'ils sont tous définis en fonction de la fonction compare. Cette fonction est à son tour définie en appelant

traits::compare(this->data(), str.data(), rlen)

str est la chaîne à laquelle vous comparez et rlen est la plus petite des deux longueurs de chaîne. C'est en fait assez intéressant, car cela signifie que la définition de compare utilise directement la fonction compare exportée par le type de traits spécifié comme paramètre de modèle! Par conséquent, si nous définissons une nouvelle classe de traits, puis définissons compare afin de comparer les caractères sans tenir compte de la casse, nous pouvons construire une classe de chaînes qui se comporte exactement comme std::string, mais traite les choses sans tenir compte de la casse!

Voici un exemple. Nous héritons de std::char_traits<char> pour obtenir le comportement par défaut de toutes les fonctions que nous n'écrivons pas:

class CaseInsensitiveTraits: public std::char_traits<char> {
public:
    static bool lt (char one, char two) {
        return std::tolower(one) < std::tolower(two);
    }

    static bool eq (char one, char two) {
        return std::tolower(one) == std::tolower(two);
    }

    static int compare (const char* one, const char* two, size_t length) {
        for (size_t i = 0; i < length; ++i) {
            if (lt(one[i], two[i])) return -1;
            if (lt(two[i], one[i])) return +1;
        }
        return 0;
    }
};

(Remarquez que j'ai également défini eq et lt ici, qui comparent les caractères pour l'égalité et moins que, respectivement, puis défini compare en fonction de cette fonction).

Maintenant que nous avons cette classe de traits, nous pouvons définir CaseInsensitiveString trivialement comme

typedef std::basic_string<char, CaseInsensitiveTraits> CaseInsensitiveString;

Et le tour est joué! Nous avons maintenant une chaîne qui traite tout sans tenir compte de la casse!

Bien sûr, il existe d'autres raisons à cela pour utiliser des traits. Par exemple, si vous souhaitez définir une chaîne qui utilise un type de caractère sous-jacent d'une taille fixe, vous pouvez spécialiser char_traits sur ce type, puis créez des chaînes à partir de ce type. Dans l'API Windows, par exemple, il y a un type TCHAR qui est soit un caractère étroit ou large selon les macros que vous définissez pendant le prétraitement. Vous pouvez ensuite créer des chaînes à partir de TCHARs en écrivant

typedef basic_string<TCHAR> tstring;

Et maintenant, vous avez une chaîne de TCHARs.

Dans tous ces exemples, notez que nous venons de définir une classe de traits (ou en avons utilisé une qui existait déjà) comme paramètre d'un type de modèle afin d'obtenir une chaîne pour ce type. L'intérêt de tout cela est que le basic_string l'auteur a juste besoin de spécifier comment utiliser les traits et nous pouvons par magie leur faire utiliser nos traits plutôt que la valeur par défaut pour obtenir des chaînes qui ont des nuances ou des bizarreries qui ne font pas partie du type de chaîne par défaut.

J'espère que cela t'aides!

[~ # ~] edit [~ # ~] : Comme l'a souligné @phooji, cette notion de traits n'est pas seulement utilisée par la STL, ni il est spécifique au C++. Comme une auto-promotion complètement éhontée, il y a quelque temps, j'ai écrit ne implémentation d'un arbre de recherche ternaire (un type d'arbre radix décrit ici ) qui utilise des traits pour stocker des chaînes de n'importe quel type et en utilisant le type de comparaison que le client souhaite qu'ils stockent. Ce pourrait être une lecture intéressante si vous voulez voir un exemple d'utilisation dans la pratique.

[~ # ~] modifier [~ # ~] : en réponse à votre affirmation selon laquelle std::string n'utilise pas traits::length, il s'avère que c'est le cas à quelques endroits. Plus particulièrement, lorsque vous construisez un std::string sur un char* Chaîne de style C, la nouvelle longueur de la chaîne est dérivée en appelant traits::length sur cette chaîne. Il paraît que traits::length est principalement utilisé pour traiter les séquences de caractères de style C, qui sont le "dénominateur le moins commun" des chaînes en C++, tandis que std::string est utilisé pour travailler avec des chaînes de contenu arbitraire.

157
templatetypedef