Cours de C/C++


précédentsommairesuivant

14. Les types complémentaires

Le C++ étant un langage basé sur le C, il souffre des mêmes limitations concernant les types de données avancés que celui-ci. Pour pallier cet inconvénient, la bibliothèque standard C++ définit des types complémentaires sous forme de classes C++, éventuellement template, et permettant de satisfaire aux besoins les plus courants. Parmi ces types, on notera avant tout le type basic_string, qui permet de manipuler les chaînes de caractères de manière plus simple et plus sûre qu'avec des pointeurs et des tableaux de caractères. Mais la bibliothèque standard définit également des classes utilitaires qui permettent de manipuler les autres types plus facilement, ainsi que des types capables d'utiliser toutes les ressources de la machine pour les calculs numériques avancés.

14.1. Les chaînes de caractères

La classe template basic_string de la bibliothèque standard, déclarée dans l'en-tête string, facilite le travail du programmeur et permet d'écrire du code manipulant des textes de manière beaucoup plus sûre. En effet, cette classe encapsule les chaînes de caractères C classiques et fournissent des services extrêmement intéressants qui n'étaient pas disponibles auparavant.

En particulier, la classe basic_string dispose des caractéristiques suivantes :
  • compatibilité quasi-totale avec les chaînes de caractères C standards ;
  • gestion des chaînes à taille variable ;
  • prise en charge de l'allocation dynamique de la mémoire et de sa libération en fonction des besoins et de la taille des chaînes manipulées ;
  • définition des opérateurs de concaténation, de comparaison et des principales méthodes de recherche dans les chaînes de caractères ;
  • intégration totale dans la bibliothèque standard, en particulier au niveau des flux d'entrée / sortie.

Comme il l'a été dit plus haut, la classe basic_string est une classe template. Cela signifie qu'elle est capable de prendre en charge des chaînes de n'importe quel type de caractère. Pour cela, elle ne se base que sur la classe des traits du type de caractère manipulé. Il est donc parfaitement possible de l'utiliser avec des types définis par l'utilisateur, pourvu que la classe des traits des caractères soit définie pour ces types. Bien entendu, la classe basic_string peut être utilisée avec les types de caractères du langage, à savoir char et wchar_t.

Les déclarations de l'en-tête string sont essentiellement les suivantes :

 
Sélectionnez

template <class charT, class traits = char_traits<charT>,
    class Allocator = allocator<charT> >
class basic_string
{
public:
    // Types
    typedef traits                     traits_type;
    typedef typename traits::char_type value_type;
    typedef Allocator                  allocator_type;
    typedef typename Allocator::size_type       size_type;
    typedef typename Allocator::difference_type difference_type;
    typedef typename Allocator::reference       reference_type;
    typedef typename Allocator::const_reference const_reference;
    typedef typename Allocator::pointer         pointer;
    typedef typename Allocator::const_pointer   const_pointer;
 
    // Constante utilisée en interne et représentant la valeur maximale
    // du type size_type :
    static const size_type npos = static_cast<size_type>(-1);
 
    // Constructeurs et destructeur :
    explicit basic_string(const Allocator &allocateur = Allocator());
    basic_string(const basic_string &source, size_type debut = 0,
        size_type longueur = npos, const Allocator &allocateur = Allocator());
    basic_string(const charT *chaine, size_type nombre,
        const Allocator &allocateur = Allocator());
    basic_string(const charT *chaine,
        const Allocator &allocateur = Allocator());
    basic_string(size_type nombre, charT caractere,
        const Allocator &allocateur  = Allocator());
    template <class InputIterator>
    basic_string(InputIterator debut, InputIterator fin,
        const Allocator &allocateur  = Allocator());
    ~basic_string();
 
    // Itérateurs :
    typedef type_privé iterator;
    typedef type_privé const iterator;
    typedef std::reverse_iterator<iterator> reverse_iterator;
    typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
    iterator       begin();
    const_iterator begin() const;
    iterator       end();
    const_iterator end() const;
    reverse_iterator       rbegin();
    const_reverse_iterator rbegin() const;
    reverse_iterator       rend();
    const_reverse_iterator rend() const;
 
    // Accesseurs :
    size_type size() const;
    size_type length() const;
    size_type max_size() const;
    size_type capacity() const;
    bool empty() const;
    allocator_type get_allocator() const;
 
    // Manipulateurs :
    void resize(size_type taille, charT caractere);
    void resize(size_type taille);
    void reserve(size_type taille = 0);
 
    // Accès aux données de la chaîne :
    const_reference operator[](size_type index) const;
    reference operator[](size_type index);
    const_reference at(size_type index) const;
    reference at(size_type index);
    const charT *c_str() const;
    const charT *data() const;
    size_type copy(charT *destination, size_type taille,
        size_type debut = 0) const;
    basic_string substr(size_type debut = 0, size_type taille = npos) const;
 
    // Affectation :
    basic_string &operator=(const basic_string &source);
    basic_string &operator=(const charT *source);
    basic_string &operator=(charT caractere);
    basic_string &assign(const basic_string &source);
    basic_string &assign(const basic_string &source,
        size_type position, size_type nombre);
    basic_string &assign(const charT *chaine, size_type nombre);
    basic_string &assign(const charT *chaine);
    basic_string &assign(size_type nombre, charT caractere);
    template <class InputIterator>
    basic_string &assign(InputIterator debut, InputIterator fin);
 
    // Concaténation et ajout :
    basic_string &operator+=(const basic_string &source);
    basic_string &operator+=(const charT *chaine);
    basic_string &operator+=(charT caractere);
    basic_string &append(const basic_string &source);
    basic_string &append(const basic_string &source,
        size_type position, size_type nombre);
    basic_string &append(const charT *chaine, size_type nombre);
    basic_string &append(const charT *chaine);
    basic_string &append(size_type nombre, charT caractere);
    template <class InputIterator>
    basic_string &append(InputIterator debut, InputIterator fin);
 
    // Insertion et extraction :
    basic_string &insert(size_type position, const basic_string &source);
    basic_string &insert(size_type position, const basic_string &source,
        size_type debut, size_type nombre);
    basic_string &insert(size_type position, const charT *chaine,
        size_type nombre);
    basic_string &insert(size_type position, const charT *chaine);
    basic_string &insert(size_type position, size_type nombre,
        charT caractere);
    iterator insert(iterator position, charT caractere = charT());
    void insert(iterator position, size_type nombre, charT caractere);
    template <class InputIterator>
    void insert(iterator position, InputIterator debut, InputIterator fin);
 
    // Suppression :
    basic_string &erase(size_type debut = 0, size_type longueur = npos);
    iterator erase(iterator position);
    iterator erase(iterator debut, iterator fin);
    void clear();
 
    // Remplacement et échange :
    basic_string &replace(size_type position, size_type longueur,
        const basic_string &remplacement);
    basic_string &replace(size_type position, size_type longueur,
        const basic_string &remplacement, size_type debut,
        size_type taille);
    basic_string &replace(size_type position, size_type longueur,
        const charT *remplacement, size_type taille);
    basic_string &replace(size_type position, size_type longueur,
        const charT *remplacement);
    basic_string &replace(size_type position, size_type longueur,
        size_type nombre, charT caractere);
    basic_string &replace(iterator debut, iterator fin,
        const basic_string &remplacement);
    basic_string &replace(iterator debut, iterator fin,
        const charT *remplacement, size_type taille);
    basic_string &replace(iterator debut, iterator fin,
        const charT *remplacement);
    basic_string &replace(iterator debut, iterator fin,
        size_type nombre, charT caractere);
    template <class InputIterator>
    basic_string &replace(iterator debut, iterator fin,
        InputIterator debut_remplacement, InputIterator fin_remplacement);
    void swap(basic_string<charT, traits, Allocator> &chaine);
 
    // Comparaison :
    int compare(const basic_string &chaine) const;
    int compare(size_type debut1, size_type longueur1,
        const basic_string &chaine,
        size_type debut2, size_type longueur2) const;
    int compare(const charT *chaine) const;
    int compare(size_type debut, size_type longueur, const charT *chaine,
        size_type taille = npos) const;
 
    // Recherche :
    size_type find(const basic_string &motif,
        size_type position = 0) const;
    size_type find(const charT *motif, size_type position,
        size_type taille) const;
    size_type find(const charT *motif, size_type position = 0) const;
    size_type find(charT caractere, size_type position = 0) const;
    size_type rfind(const basic_string &motif,
        size_type position = npos) const;
    size_type rfind(const charT *motif, size_type position,
        size_type taille) const;
    size_type rfind(const charT *motif, size_type position = npos) const;
    size_type rfind(charT caractere, size_type position = npos) const;
    size_type find_first_of(const basic_string &motif,
        size_type position = 0) const;
    size_type find_first_of(const charT *motif, size_type position,
        size_type taille) const;
    size_type find_first_of(const charT *motif,
        size_type position = 0) const;
    size_type find_first_of(charT caractere, size_type position = 0) const;
    size_type find_last_of(const basic_string &motif,
        size_type position = npos) const;
    size_type find_last_of(const charT *motif, size_type position,
        size_type taille) const;
    size_type find_last_of(const charT *motif,
        size_type position = npos) const;
    size_type find_last_of(charT caractere,
        size_type position = npos) const;
    size_type find_first_not_of(const basic_string &motif,
        size_type position = 0) const;
    size_type find_first_not_of(const charT *motif, size_type position,
        size_type taille) const;
    size_type find_first_not_of(const charT *motif,
        size_type position = 0) const;
    size_type find_first_not_of(charT caractere,
        size_type position = 0) const;
    size_type find_last_not_of(const basic_string &motif,
        size_type position = npos) const;
    size_type find_last_not_of(const charT *motif, size_type position,
        size_type taille) const;
    size_type find_last_not_of(const charT *motif,
        size_type position = npos) const;
    size_type find_last_not_of(charT caractere,
        size_type position = npos) const;
};
 
typedef basic_string<char>    string;
typedef basic_string<wchar_t> wstring;
 

Les opérateurs de concaténation, de comparaison et de sérialisation dans les flux d'entrée / sortie sont également définis dans cet en-tête et n'ont pas été reportés ici par souci de clarté. Comme vous pouvez le voir, la classe basic_string dispose d'un grand nombre de méthodes. Nous allons à présent les détailler dans les paragraphes suivants.

La bibliothèque standard définit deux types chaînes de caractères pour les types standards de caractères du langage : le type string pour les char, et le type wstring pour les wchar_t. En pratique, ce seront donc ces types qui seront utilisés dans les programmes. Les exemples de la suite de ce document utiliseront donc le type string, mais vous êtes libre d'utiliser des instances de la classe basic_string pour d'autres types de caractères.

14.1.1. Construction et initialisation d'une chaîne

La manière la plus simple de construire une basic_string est simplement de la déclarer, sans paramètres. Cela a pour conséquence d'appeler le constructeur par défaut, et d'initialiser la chaîne à la chaîne vide.

En revanche, si vous désirez initialiser cette chaîne, plusieurs possibilités s'offrent à vous. Outre le constructeur de copie, qui permet de copier une autre basic_string, il existe plusieurs surcharges du constructeur permettant d'initialiser la chaîne de différentes manières. Le constructeur le plus utilisé sera sans aucun doute le constructeur qui prend en paramètre une chaîne de caractères C classique :

 
Sélectionnez

string chaine("Valeur initiale");
 

Il existe cependant une variante de ce constructeur, qui prend en paramètre le nombre de caractères de la chaîne source à utiliser pour l'initialisation de la basic_string. Ce constructeur devra être utilisé dans le cas des tableaux de caractères, qui contiennent des chaînes de caractères qui ne sont pas nécessairement terminées par un caractère nul :

 
Sélectionnez

string chaine("Valeur initiale", 6);
 

La ligne précédente initialise la chaîne chaine avec la chaîne de caractères "Valeur", car seuls les six premiers caractères de la chaîne d'initialisation sont utilisés.

Il est également possible d'initialiser une basic_string avec une partie de chaîne de caractères seulement. Pour cela, il faut utiliser le constructeur template, qui prend en paramètre un itérateur référençant le premier caractère à utiliser lors de l'initialisation de la basic_string et un itérateur référençant le caractère suivant le dernier caractère à utiliser. Bien entendu, ces deux itérateurs sont de simples pointeurs de caractères si les caractères devant servir à l'initialisation sont dans une chaîne de caractères C ou dans un tableau de caractères. Cependant, ce peut être des itérateurs d'un conteneur quelconque, pourvu que celui-ci contienne bien une séquence de caractères et que le deuxième itérateur se trouve bien après le premier dans cette séquence. Notez que le deuxième itérateur ne référence pas le dernier caractère de la séquence d'initialisation, mais bien le caractère suivant. Il peut donc valoir la valeur de fin de l'itérateur du conteneur source. Ainsi, le code suivant :

 
Sélectionnez

char *source = "Valeur initiale";
string s(source, source+6);
 

a strictement le même effet que celui de l'exemple précédent.

Enfin, il existe un constructeur dont le but est d'initialiser une basic_string avec une suite de caractères identiques d'une certaine longueur. Ce constructeur n'est réellement pas difficile à utiliser, puisqu'il suffit de lui fournir en paramètre le nombre de caractères que la basic_string devra contenir et la valeur du caractère qui devra être répété dans cette chaîne.

Vous remarquerez que tous ces constructeurs prennent en dernier paramètre une instance d'allocateur mémoire à utiliser pour les opérations d'allocation et de libération de la mémoire que la chaîne est susceptible d'avoir à faire. Vous pouvez spécifier une instance quelconque, ou utiliser la valeur par défaut fournie par les déclarations des constructeurs. Cette valeur par défaut est tout simplement une instance temporaire de l'allocateur spécifié en paramètre template de la classe basic_string. Par défaut, cet allocateur est l'allocateur standard, pour lequel toutes les instances permettent d'accéder à la même mémoire. Il n'est donc pas nécessaire de spécifier l'instance à utiliser, puisqu'elles sont toutes fonctionnellement identiques. En pratique donc, il est très rare d'avoir à spécifier un allocateur mémoire dans ces constructeurs.

14.1.2. Accès aux propriétés d'une chaîne

La classe basic_string fournit un certain nombre d'accesseurs permettant d'obtenir des informations sur son état et sur la chaîne de caractères qu'elle contient. L'une des informations les plus intéressantes est sans nul doute la longueur de cette chaîne. Elle peut être obtenue à l'aide de deux accesseurs, qui sont strictement équivalents : size et length. Vous pouvez utiliser l'un ou l'autre, selon votre convenance. Par ailleurs, si vous désirez simplement savoir si la basic_string est vide, vous pouvez appeler la méthode empty.

Note : Attention, contrairement à ce que son nom pourrait laisser penser, la méthode empty ne vide pas la basic_string !

La taille maximale qu'une basic_string peut contenir est souvent directement liée à la quantité de mémoire disponible, puisque la chaîne de caractères qu'elle contient est allouée dynamiquement. Il n'y a donc souvent pas beaucoup d'intérêt à obtenir cette taille, mais vous pouvez malgré tout le faire, grâce à la méthode max_size.

La quantité de mémoire réellement allouée par une basic_string peut être supérieure à la longueur de la chaîne de caractères contenue. En effet, la classe basic_string peut conserver une marge de manoeuvre, pour le cas où la chaîne devrait être agrandie à la suite d'une opération particulière. Cela permet de réduire les réallocations de mémoire, qui peuvent être très coûteuses lorsque la mémoire se fragmente (la chaîne doit être recopiée vers un nouvel emplacement si un autre bloc mémoire se trouve juste après le bloc mémoire à réallouer). Cette quantité de mémoire peut être obtenue à l'aide de la méthode capacity. Nous verrons comment réserver de la place mémoire en prévision d'un redimensionnement ultérieur dans la section suivante.

Dans le cas où vous utiliseriez un allocateur différent de l'allocateur par défaut, vous pouvez obtenir une copie de cet allocateur grâce à la méthode get_allocator. Il est relativement rare d'avoir à utiliser cette méthode.

14.1.3. Modification de la taille des chaînes

Une fois qu'une basic_string a été construite, il est possible de la modifier a posteriori pour la réduire, l'agrandir ou augmenter sa capacité. Ces opérations peuvent être réalisées à l'aide de méthodes fournies à cet effet.

La méthode resize permet de modifier la taille de la chaîne de caractères stockée dans la basic_string. Dans sa version la plus simple, elle prend en paramètre la nouvelle taille que la chaîne doit avoir. Si cette taille est inférieure à la taille courante, la chaîne est tronquée. En revanche, si cette taille est supérieure à la taille actuelle, la chaîne est étendue et les nouveaux caractères sont initialisés avec la valeur des caractères définie par le constructeur par défaut de leur classe. Pour les types prédéfinis char et wchar_t, cette valeur est toujours le caractère nul. Les données stockées dans la basic_string représentent donc toujours la même chaîne de caractères C, puisque ces chaînes utilisent le caractère nul comme marqueur de fin de chaîne. Ainsi, la longueur renvoyée par la méthode size peut être différente de la longueur de la chaîne C contenue par la basic_string.

Exemple 14-1. Redimensionnement d'une chaîne
Sélectionnez

#include <iostream>
#include <string>
#include <string.h>
 
using namespace std;
 
int main(void)
{
    string s("123");
    s.resize(10);
    cout << s << endl;
    // La nouvelle taille vaut bien 10 :
    cout << "Nouvelle taille : " << s.length() << endl;
    // mais la longueur de la chaîne C reste 3 :
    cout << "Longueur C : " << strlen(s.c_str()) << endl;
    return 0;
}
 

Note : La méthode c_str utilisée dans cet exemple sera décrite en détail dans la section suivante. Elle permet d'obtenir l'adresse de la chaîne C stockée en interne dans la basic_string.

La fonction strlen quant à elle est une des fonctions de manipulation des chaînes de caractères de la bibliothèque C. Elle est définie dans le fichier d'en-tête string.h (que l'on ne confondra pas avec l'en-tête string de la bibliothèque standard C++), et renvoie la longueur de la chaîne de caractères qui lui est fournie en paramètre.

Si la valeur par défaut utilisée pour les caractères complémentaires dans la méthode resize n'est pas celle qui est désirée, il faut en utiliser une autre version. Cette deuxième version prend, en plus de la nouvelle taille de la chaîne de caractères, le caractère de remplissage à utiliser pour le cas où la nouvelle taille serait supérieure à la taille initiale de la chaîne :

 
Sélectionnez

string s("123");
s.resize(10, 'a');
 

Dans cet exemple, s contient finalement la chaîne de caractères "123aaaaaaa".

Nous avons vu précédemment que les basic_string étaient susceptibles d'allouer plus de mémoire que nécessaire pour stocker leurs données afin de limiter le nombre de réallocation mémoire. Ce mécanisme est complètement pris en charge par la bibliothèque, et le programmeur n'a en général pas à s'en soucier. Cependant, il peut exister des situations où l'on sait à l'avance la taille minimale qu'une chaîne doit avoir pour permettre de travailler dessus sans craindre de réallocations mémoire successives. Dans ce cas, on a tout intérêt à fixer la capacité de la chaîne directement à cette valeur, afin d'optimiser les traitements. Cela est réalisable à l'aide de la méthode reserve. Cette méthode prend en paramètre la capacité minimale que la basic_string doit avoir. La nouvelle capacité n'est pas forcément égale à ce paramètre après cet appel, car la basic_string peut allouer plus de mémoire que demandé.

Exemple 14-2. Réservation de mémoire dans une chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s;
    cout << s.capacity() << endl;
    s.reserve(15);
    s = "123";
    cout << s.capacity() << endl;
    return 0;
}
 

Note : Les méthodes resize et reserve peuvent effectuer une réallocation de la zone mémoire contenant la chaîne de caractères. Par conséquent, toutes les références sur les caractères de la chaîne et tous les itérateurs sur cette chaîne deviennent invalide à la suite de leur exécution.

14.1.4. Accès aux données de la chaîne de caractères

Les caractères des basic_string peuvent être accédés de nombreuses manières. Premièrement, la classe basic_string surcharge l'opérateur d'accès aux éléments d'un tableau, et l'on pourra les utiliser pour obtenir une référence à un des caractères de la chaîne à partir de son indice. Cet opérateur n'est défini que pour les indices valides dans la chaîne de caractères, à savoir les indices variant de 0 à la valeur retournée par la méthode size de la chaîne moins un :

 
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s("azc");
    // Remplace le deuxième caractère de la chaîne par un 'b' :
    s[1] = 'b';
    cout << s << endl;
    return 0;
}
 

Lorsqu'il est appliqué à une basic_string constante, l'opérateur tableau peut renvoyer la valeur du caractère dont l'indice est exactement la taille de la chaîne. Il s'agit évidemment du caractère nul servant de marqueur de fin de chaîne. En revanche, la référence renvoyée par cet opérateur pour toutes les autres valeurs, ainsi que par l'opérateur tableau appliqué aux chaînes non constante pour le caractère de fin de chaîne ne sont pas valides. Le comportement des programmes qui effectuent de tels accès est imprévisible.

Il existe une autre possibilité pour accéder aux caractères d'une basic_string. Il s'agit de la méthode at. Contrairement à l'opérateur tableau, cette méthode permet d'effectuer un contrôle sur la validité de l'indice utilisé. Elle renvoie, comme l'opérateur de tableau de la classe basic_string, la référence du caractère dont l'indice est spécifié en paramètre. Cependant, elle effectue au préalable un contrôle sur la validité de cet indice, qui doit toujours être strictement inférieur à la taille de la chaîne. Dans le cas contraire, la méthode at lance une exception out_of_range :

 
Sélectionnez

#include <iostream>
#include <string>
#include <stdexcept>
 
using namespace std;
 
int main(void)
{
    string s("01234");
    try
    {
        s.at(5);
    }
    catch (const out_of_range &)
    {
        cout << "Débordement !" << endl;
    }
    return 0;
}
 

La classe basic_string ne contient pas d'opérateur de transtypage vers les types des chaînes de caractères C classique, à savoir le type pointeur sur caractère et pointeur sur caractère constant. C'est un choix de conception, qui permet d'éviter les conversions implicites des basic_string en chaîne C classique, qui pourraient être extrêmement dangereuses. En effet, ces conversions conduiraient à obtenir implicitement des pointeurs qui ne seraient plus valides dès qu'une opération serait effectuée sur la basic_string source. Cependant, la classe basic_string fournit deux méthodes permettant d'obtenir de tels pointeurs de manière explicite. Le programmeur prend donc ses responsabilités lorsqu'il utilise ces méthodes, et est supposé savoir dans quel contexte ces pointeurs sont valides.

Ces deux méthodes sont respectivement la méthode data et la méthode c_str. La première méthode renvoie un pointeur sur les données de la basic_string. Ces données ne sont rien d'autre qu'un tableau de caractères, dont la dimension est exactement la valeur retournée par la méthode size ou par la méthode length. Ce tableau peut contenir des caractères de terminaison de chaîne, si par exemple une telle valeur a été écrite explicitement ou a été introduite suite à un redimensionnement de la chaîne. La méthode c_str en revanche retourne un pointeur sur la chaîne de caractères C contenue dans la basic_string. Contrairement aux données renvoyées par la méthode data, cette chaîne est nécessairement terminée par un caractère de fin de chaîne. Cette méthode sera donc utilisée partout où l'on veut obtenir une chaîne de caractères C classique, mais elle ne devra pas être utilisée pour accéder aux données situées après ce caractère de fin de chaîne.

Exemple 14-3. Accès direct aux données d'une chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s("123456");
    // La chaîne C est coupée au troisième caractère :
    s[3] = 0;
    cout << s.c_str() << endl;
    // Mais on peut quand même obtenir les caractères suivants :
    cout << s.data()[4] << s.data()[5] << endl;
    return 0;
}
 

Notez que ces méthodes renvoient des pointeurs sur des données constantes. Cela est normal, car il est absolument interdit de modifier les données internes à la basic_string qui a fourni ces pointeurs. Vous ne devez donc en aucun cas essayer d'écrire dans ces tableaux de caractères, même en faisant un transtypage au préalable.

Enfin, la classe basic_string définit des itérateurs à accès aléatoires permettant d'accéder à ses données comme s'il s'agissait d'une chaîne de caractères standard. Les méthodes begin et end permettent d'obtenir respectivement un itérateur sur le premier caractère de la chaîne et la valeur de fin de ce même itérateur. De même, les méthodes rbegin et rend permettent de parcourir les données de la basic_string en sens inverse. Prenez garde au fait que ces itérateurs ne sont valides que tant qu'aucune méthode modifiant la chaîne n'est appelée.

14.1.5. Opérations sur les chaînes

La classe basic_string fournit tout un ensemble de méthodes permettant d'effectuer les opérations les plus courantes sur les chaînes de caractères. C'est cet ensemble de méthodes qui font tout l'intérêt de cette classe par rapport aux chaînes de caractères classiques du langage C, car elles prennent en charge automatiquement les allocations mémoire et les copies de chaînes que l'implémentation de ces fonctionnalités peut imposer. Ces opérations comprennent l'affectation et la concaténation de deux chaînes de caractères, l'extraction d'une sous-chaîne, ainsi que la suppression et le remplacement de caractères dans une chaîne.

14.1.5.1. Affectation et concaténation de chaînes de caractères

Plusieurs surcharges de l'opérateur d'affectation sont définies dans la classe basic_string. Ces surcharges permettent d'affecter une nouvelle valeur à une chaîne, en remplaçant éventuellement l'ancienne valeur. Dans le cas d'un remplacement, la mémoire consommée par l'ancienne valeur est automatiquement réutilisée ou libérée si une réallocation est nécessaire. Ces opérateurs d'affectation peuvent prendre en paramètre une autre basic_string, une chaîne de caractères C classique ou même un simple caractère. Leur utilisation est donc directe, il suffit simplement d'écrire une affectation normale.

Il est impossible, avec l'opérateur d'affectation, de fournir des paramètres supplémentaires comme ceux dont les constructeurs de la classe basic_string disposent par exemple. C'est pour cette raison qu'une autre méthode, la méthode assign, a été définie pour permettre de faire des affectations plus complexes.

Les premières versions de ces méthodes permettent bien entendu d'effectuer l'affectation d'une autre basic_string ou d'une chaîne de caractères C classique. Cependant, il est également possible de spécifier la longueur de la portion de chaîne à copier en deuxième paramètre pour les chaînes C, et la position ainsi que le nombre de caractères à copier dans le cas d'une basic_string. Il est également possible de réinitialiser une basic_string avec un caractère spécifique, en donnant et le nombre de caractères à dupliquer dans la chaîne en premier paramètre et la valeur de ce caractère en deuxième paramètre. Enfin, il existe une version template de cette méthode permettant d'affecter à la basic_string la séquence de caractères compris entre deux itérateurs d'un conteneur.

Exemple 14-4. Affectation de chaîne de caractères
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1, s2;
    char *p="1234567890";
    // Affecte "345" à s1 :
    s1.assign(p+2, p+6);
    cout << s1 << endl;
    // Affecte les deux premiers caractères de s1 à s2 :
    s2.assign(s1, 0, 2);
    cout << s2 << endl;
    // Réinitialise s1 avec des 'A' :
    s1.assign(5, 'A');
    cout << s1 << endl;
    return 0;
}
 

De manière similaire, l'opérateur d'affectation avec addition '+=' a été surchargé afin de permettre les concaténations de chaînes de caractères de manière intuitive. Cet opérateur permet d'ajouter aussi bien une autre basic_string qu'une chaîne de caractères C classique ou un unique caractère à la fin d'une basic_string. Comme cet opérateur est trop restrictif lorsqu'il s'agit de concaténer une partie seulement d'une autre chaîne à une basic_string, un jeu de méthodes append a été défini. Ces méthodes permettent d'ajouter à une basic_string une autre basic_string ou une chaîne de caractères C bien entendu, mais aussi d'ajouter une partie seulement de ces chaînes ou un nombre déterminé de caractères. Toutes ces méthodes prennent les mêmes paramètres que les méthodes assign correspondantes et leur emploi ne devrait pas poser de problème particulier.

Exemple 14-5. Concaténation de chaînes de carctères
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1 = "abcef";
    string s2 = "ghijkl";
    // Utilisation de l'opérateur de concaténation :
    s1+=s2;
    cout << s1 << endl;
    // Utilisation de la méthode append :
    s1.append("mnopq123456", 5);
    cout << s1 << endl;
    return 0;
}	
 

14.1.5.2. Extraction de données d'une chaîne de caractères

Nous avons vu que les méthodes data et c_str permettaient d'obtenir des pointeurs sur les données des basic_string. Cependant, il est interdit de modifier les données de la basic_string au travers de ces pointeurs. Or, il peut être utile, dans certaines situations, d'avoir à travailler sur ces données, il faut donc pouvoir en faire une copie temporaire. C'est ce que permet la méthode copy. Cette fonction prend en paramètre un pointeur sur une zone de mémoire devant accueillir la copie des données de la basic_string, le nombre de caractères à copier, ainsi que le numéro du caractère de la basic_string à partir duquel la copie doit commencer. Ce dernier paramètre est facultatif, car il prend par défaut la valeur 0 (la copie se fait donc à partir du premier caractère de la basic_string.

Exemple 14-6. Copie de travail des données d'une basic_string
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s="1234567890";
    // Copie la chaîne de caractères dans une zone de travail :
    char buffer[16];
    s.copy(buffer, s.size(), 0);
    buffer[s.size()] = 0;
    cout << buffer << endl;
    return 0;
}
 

La basic_string doit contenir suffisamment de caractères pour que la copie puisse se faire. Si ce n'est pas le cas, elle lancera une exception out_of_range. En revanche, la méthode copy ne peut faire aucune vérification quant à la place disponible dans la zone mémoire qui lui est passée en paramètre. Il est donc de la responsabilité du programmeur de s'assurer que cette zone est suffisamment grande pour accueillir tous les caractères qu'il demande.

La méthode copy permet d'obtenir la copie d'une sous-chaîne de la chaîne contenue dans la basic_string. Toutefois, si l'on veut stocker cette sous-chaîne dans une autre basic_string, il ne faut pas utiliser cette méthode. La méthode substr permet en effet d'effectuer ce travail directement. Cette méthode prend en premier paramètre le numéro du premier caractère à partir de la sous-chaîne à copier, ainsi que sa longueur. Comme pour la méthode copy, il faut que la basic_string source contienne suffisamment de caractères faute de quoi une exception out_of_range sera lancée.

Exemple 14-7. Extraction de sous-chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1 = "1234567890";
    string s2 = s1.substr(2, 5);
    cout << s2 << endl;
    return 0;					
}					
 

14.1.5.3. Insertion et suppression de caractères dans une chaîne

La classe basic_string dispose de tout un jeu de méthodes d'insertion de caractères ou de chaînes de caractères au sein d'une basic_string existante. Toutes ces méthodes sont des surcharges de la méthode insert. Ces surcharges prennent toutes un paramètre en première position qui indique l'endroit où l'insertion doit être faite. Ce paramètre peut être soit un numéro de caractère, indiqué par une valeur de type size_type, soit un itérateur de la basic_string dans laquelle l'insertion doit être faite. Les autres paramètres permettent de spécifier ce qui doit être inséré dans cette chaîne.

Les versions les plus simples de la méthode insert prennent en deuxième paramètre une autre basic_string ou une chaîne de caractères C classique. Leur contenu sera inséré à l'emplacement indiqué. Lorsque le deuxième paramètre est une basic_string, il est possible d'indiquer le numéro du premier caractère ainsi que le nombre de caractères total à insérer. De même, lors de l'insertion d'une chaîne C classique, il est possible d'indiquer le nombre de caractères de cette chaîne qui doivent être insérés.

Il existe aussi des méthodes insert permettant d'insérer un ou plusieurs caractères à un emplacement donné dans la chaîne de caractères. Ce caractère doit alors spécifié en deuxième paramètre, sauf si l'on veut insérer plusieurs caractères identiques dans la chaîne, auquel cas on doit indiquer le nombre de caractères à insérer et la valeur de ce caractère.

Enfin, il existe une version de la méthode insert qui prend en paramètre, en plus de l'itérateur spécifiant la position à partir de laquelle l'insertion doit être faite dans la basic_string, deux autres itérateurs d'un quelconque conteneur contenant des caractères. Utilisé avec des pointeurs de caractères, cet itérateur peut être utilisé pour insérer un morceau quelconque de chaîne de caractères C dans une basic_string.

Toutes ces méthodes renvoient généralement la basic_string sur laquelle ils travaillent, sauf les méthodes qui prennent en paramètre un itérateur. Ces méthodes supposent en effet que la manipulation de la chaîne de caractères se fait par l'intermédiaire de cet itérateur, et non par l'intermédiaire d'une référence sur la basic_string. Cependant, la méthode insert permettant d'insérer un caractère unique à un emplacement spécifié par un itérateur renvoie la valeur de l'itérateur référençant le caractère qui vient d'être inséré afin de permettre de récupérer ce nouvel itérateur pour les opérations ultérieures.

Exemple 14-8. Insertion de caractères dans une chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s = "abef";
    // Insère un 'c' et un 'd' :
    s.insert(2, "cdze", 2);
    // Idem pour 'g' et 'h', mais avec une basic_string :
    string gh = "gh";
    s.insert(6, gh);
    cout << s << endl;
    return 0;
}
 

Il existe également trois surcharges de la méthode erase, dont le but est de supprimer des caractères dans une chaîne en décalant les caractères suivant les caractères supprimés pour remplir les positions ainsi libérées. La première méthode erase prend en premier paramètre la position du premier caractère et le nombre des caractères à supprimer. La deuxième méthode fonctionne de manière similaire, mais prend en paramètre l'itérateur de début et l'itérateur de fin de la sous-chaîne de caractères qui doit être supprimée. Enfin, la troisième version de erase permet de supprimer un unique caractère, dont la position est spécifiée encore une fois par un itérateur. Ces deux dernières méthodes renvoient l'itérateur référençant le caractère suivant le dernier caractère qui a été supprimé de la chaîne. S'il n'y avait pas de caractères après le dernier caractère effacé, l'itérateur de fin de chaîne est renvoyé.

Enfin, il existe une méthode dédiée pour l'effacement complet de la chaîne de caractères contenue dans une basic_string. Cette méthode est la méthode clear.

Exemple 14-9. Suppression de caractères dans une chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s = "abcdeerrfgh";
    // Supprime la faute de frappe :
    s.erase(5,3);
    cout << s << endl;
    // Efface la chaîne de caractères complète :
    s.clear();
    if (s.empty()) cout << "Vide !" << endl;
    return 0;
}
 

14.1.5.4. Remplacements de caractères d'une chaîne

Comme pour l'insertion de chaînes de caractères, il existe tout un jeu de fonctions permettant d'effectuer un remplacement d'une partie de la chaîne de caractères stockée dans les basic_string par une autre chaîne de caractères. Ces méthodes sont nommées replace et sont tout à fait similaires dans le principe aux méthodes insert. Cependant, contrairement à celles-ci, les méthodes replace prennent un paramètre supplémentaire pour spécifier la longueur ou le caractère de fin de la sous-chaîne à remplacer. Ce paramètre doit être fourni juste après le premier paramètre, qui indique toujours le caractère de début de la sous-chaîne à remplacer. Il peut être de type size_type pour les versions de replace qui travaillent avec des indices, ou être un itérateur, pour les versions de replace qui travaillent avec des itérateurs. Les autres paramètres des fonctions replace permettent de décrire la chaîne de remplacement, et fonctionnent exactement comme les paramètres correspondants des fonctions insert.

Exemple 14-10. Remplacement d'une sous-chaîne dans une chaîne
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s = "abcerfg";
    // Remplace le 'e' et le 'r' par un 'd' et un 'e' :
    s.replace(3, 2, "de");
    cout << s << endl;
    return 0;
}
 

Dans le même ordre d'idée que le remplacement, on trouvera la méthode swap de la classe basic_string, qui permet d'intervertir le contenu de deux chaînes de caractères. Cette méthode prend en paramètre une référence sur la deuxième chaîne de caractères, avec laquelle l'interversion doit être faite. La méthode swap pourra devra être utilisée de préférence pour réaliser les échanges de chaînes de caractères, car elle est optimisée et effectue en fait l'échange par référence. Elle permet donc d'éviter de faire une copie temporaire de la chaîne destination et d'écraser la chaîne source avec cette copie.

Exemple 14-11. Échange du contenu de deux chaînes de caractères
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1 = "abcd";
    string s2 = "1234";
    cout << "s1 = " << s1 << endl;
    cout << "s2 = " << s2 << endl;
    // Intervertit les deux chaînes :
    s1.swap(s2);
    cout << "s1 = " << s1 << endl;
    cout << "s2 = " << s2 << endl;
    return 0;
}
 

14.1.6. Comparaison de chaînes de caractères

La comparaison des basic_string se base sur la méthode compare, dont plusieurs surcharges existent afin de permettre des comparaisons diverses et variées. Les deux versions les plus simples de la méthode compare prennent en paramètre soit une autre basic_string, soit une chaîne de caractères C classique. Elles effectuent donc la comparaison de la basic_string sur laquelle elles s'appliquent avec ces chaînes. Elles utilisent pour cela la méthode eq de la classe des traits des caractères utilisés par la chaîne. Si les deux chaînes ne diffèrent que par leur taille, la chaîne la plus courte sera déclarée inférieure à la chaîne la plus longue.

Les deux autres méthodes compare permettent d'effectuer la comparaison de sous-chaînes de caractères entre elles. Elles prennent toutes les deux l'indice du caractère de début et l'indice du caractère de fin de la sous-chaîne de la basic_string sur laquelle elles sont appliquées, un troisième paramètre indiquant une autre chaîne de caractères, et des indices spécifiant la deuxième sous-chaîne dans cette chaîne. Si le troisième argument est une basic_string, il faut spécifier également l'indice de début et l'indice de fin de la sous-chaîne. En revanche, s'il s'agit d'une chaîne C classique, la deuxième sous-chaîne commence toujours au premier caractère de cette chaîne, et il ne faut spécifier que la longueur de cette sous-chaîne.

La valeur renvoyée par les méthodes compare est de type entier. Cet entier est nul si les deux chaînes sont strictement égales (et de même taille), négatif si la basic_string sur laquelle la méthode compare est appliquée est plus petite que la chaîne passée en argument, soit en taille, soit au sens de l'ordre lexicographique, et positif dans le cas contraire.

Exemple 14-12. Comparaisons de chaînes de caractères
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    const char *c1 = "bcderefb";
    const char *c2 = "bcdetab";   // c2 > c1
    const char *c3 = "bcderefas"; // c3 < c1
    const char *c4 = "bcde";      // c4 < c1
    string s1 = c1;
    if (s1 < c2) cout << "c1 < c2" << endl;
    else cout << "c1 >= c2" << endl;
    if (s1.compare(c3)>0) cout << "c1 > c3" << endl;
    else cout << "c1 <= c3" << endl;
    if (s1.compare(0, string::npos, c1, 4)>0)
        cout << "c1 > c4" << endl;
    else cout << "c1 <= c4" << endl;
    return 0;
}
 

Bien entendu, les opérateurs de comparaison classiques sont également définis afin de permettre des comparaisons simples entre chaîne de caractères. Grâce à ces opérateurs, il est possible de manipuler les basic_string exactement comme les autres types ordonnés du langage. Plusieurs surcharge de ces opérateurs ont été définies et travaillent avec les différents types de données avec lesquels il est possible pour une basic_string de se comparer. L'emploi de ces opérateurs est naturel et ne pose pas de problèmes particuliers.

Note : Toutes ces comparaisons se basent sur l'ordre lexicographique du langage C. Autrement dit, les comparaisons entre chaînes de caractères ne tiennent pas compte de la locale et des conventions nationales. Elles sont donc très efficaces, mais ne pourront pas être utilisées pour comparer des chaînes de caractères humainement lisibles. Vous trouverez de plus amples renseignements sur la manière de prendre en compte les locales dans les comparaisons de chaînes de caractères dans le Chapitre 16

14.1.7. Recherche dans les chaînes

Les opérations de recherche dans les chaînes de caractères constituent une des fonctionnalités des chaînes les plus courantes. Elles constituent la plupart des opérations d'analyse des chaînes, et sont souvent le pendant de la construction et la concaténation de chaînes. La classe basic_string fournit donc tout un ensemble de méthodes permettant d'effectuer des recherches de caractères ou de sous-chaînes dans une basic_string.

Les fonctions de recherche sont toutes surchargées afin de permettre de spécifier la position à partir de laquelle la recherche doit commencer d'une part, et le motif de caractère à rechercher. Le premier paramètre indique toujours quel est ce motif, que ce soit une autre basic_string, une chaîne de caractères C classique ou un simple caractère. Le deuxième paramètre est le numéro du caractère de la basic_string sur laquelle la méthode de recherche s'applique et à partir duquelle elle commence. Ce deuxième paramètre peut être utilisé pour effectuer plusieurs recherches successives, en repartant de la dernière position trouvée à chaque fois. Lors d'une première recherche ou lors d'une recherche unique, il n'est pas nécessaire de donner la valeur de ce paramètre, car les méthodes de recherche utilisent la valeur par défaut qui convient (soit le début de la chaîne, soit la fin, selon le sens de recherche utilisé par la méthode). Les paramètres suivants permettent de donner des informations complémentaires sur le motif à utiliser pour la recherche. Il n'est utilisé que lorsque le motif est une chaîne de caractères C classique. Dans ce cas, il est en effet possible de spécifier la longueur du motif dans cette chaîne.

Les différentes fonctions de recherche disponibles sont présentées dans le tableau suivant :

Tableau 14-1. Fonctions de recherche dans les chaînes de caractères

Méthode Description
find Cette méthode permet de rechercher la sous-chaîne correspondant au motif passé en paramètre dans la basic_string sur laquelle elle est appliquée. Elle retourne l'indice de la première occurrence de ce motif dans la chaîne de caractères, ou la valeur npos si le motif n'y apparaît pas.
rfind Cette méthode permet d'effectuer une recherche similaire à celle de la méthode find, mais en parcourant la chaîne de caractères en sens inverse. Notez bien que ce n'est pas le motif qui est inversé ici, mais le sens de parcours de la chaîne. Ainsi, la méthode rfind retourne l'indice de la dernière occurrence du motif dans la chaîne, ou la valeur npos si le motif n'a pas été trouvé.
find_first_of Cette méthode permet de rechercher la première occurrence d'un des caractères présents dans le motif fourni en paramètre. Il ne s'agit donc plus d'une recherche de chaîne de caractères, mais de la recherche de tous les caractères d'un ensemble donné. La valeur retournée est l'indice du caractère trouvé, ou la valeur npos si aucun caractère du motif n'est détecté dans la chaîne.
find_last_of Cette méthode est à la méthode find_first_of ce que rfind est à find. Elle effectue la recherche du dernier caractère de la basic_string qui se trouve dans la liste des caractères du motif fourni en paramètre. La valeur retournée est l'indice de ce caractère s'il existe, et npos sinon.
find_first_not_of Cette méthode travaille en logique inverse par rapport à la méthode find_first_of. En effet, elle recherche le premier caractère de la basic_string qui n'est pas dans le motif fourni en paramètre. Elle renvoie l'indice de ce caractère, ou npos si celui-ci n'existe pas.
find_last_not_of Cette méthode effectue le même travail que la méthode find_first_not_of, mais en parcourant la chaîne de caractères source en sens inverse. Elle détermine donc l'indice du premier caractère en partant de la fin qui ne se trouve pas dans le motif fourni en paramètre. Elle renvoie npos si aucun caractère ne correspond à ce critère.
Exemple 14-13. Recherches dans les chaînes de caractères
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s = "Bonjour  tout    le monde !";
    // Recherche le mot "monde" :
    string::size_type pos = s.find("monde");
    cout << pos << endl;
    // Recherche le mot "tout" en commençant par la fin :
    pos = s.rfind("tout");
    cout << pos << endl;
    // Décompose la chaîne en mots :
    string::size_type debut = s.find_first_not_of(" \t\n");
    while (debut != string::npos)
    {
        // Recherche la fin du mot suivant :
        pos = s.find_first_of(" \t\n", debut);
        // Affiche le mot :
        if (pos != string::npos)
            cout << s.substr(debut, pos - debut) << endl;
        else
            cout << s.substr(debut) << endl;
        debut = s.find_first_not_of(" \t\n", pos);
    }
    return 0;
}
 

Note : Toutes ces fonctions de recherche utilisent l'ordre lexicographique du langage C pour effectuer leur travail. Elles peuvent donc ne pas convenir pour effectuer des recherches dans des chaînes de caractères saisies par des humains, car elles ne prennent pas en compte la locale et les paramètres nationaux de l'utilisateur. La raison de ce choix est essentiellement la recherche de l'efficacité dans la bibliothèque standard. Nous verrons dans le Chapitre 16 la manière de procéder pour prendre en compte les paramètres nationaux au niveau des chaînes de caractères.

14.1.8. Fonctions d'entrée / sortie des chaînes de caractères

Pour terminer ce tour d'horizon des chaînes de caractères, signalons que la bibliothèque standard C++ fournit des opérateurs permettant d'effectuer des écritures et des lectures sur les flux d'entrée / sortie. Les opérateurs '<<' et '>>' sont donc surchargés pour les basic_string, et permettent de manipuler celles-ci comme des types normaux. L'opérateur '<<'' permet d'envoyer le contenu de la basic_string sur le flux de sortie standard. L'opérateur '>>' lit les données du flux d'entrée standard, et les affecte à la basic_string qu'il reçoit en paramètre. Il s'arrête dès qu'il rencontre le caractère de fin de fichier, un espace, ou que la taille maximale des basic_string a été atteinte (cas improbable) :

 
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1, s2;
    cin >> s1;
    cin >> s2;
    cout << "Premier mot : " << endl << s1 << endl;
    cout << "Deuxième mot : " << endl << s2 << endl;
    return 0;
}
 

Cependant, ces opérateurs peuvent ne pas s'avérer suffisants. En effet, l'une des principales difficultés dans les programmes qui manipulent des chaînes de caractères est de lire les données qui proviennent d'un flux d'entrée ligne par ligne. La notion de ligne n'est pas très claire, et dépend fortement de l'environnement d'exécution. La bibliothèque standard C++ suppose, quant à elle, que les lignes sont délimitées par un caractère spécial servant de marqueur spécial. Généralement, ce caractère est le caractère '\n', mais il est également possible d'utiliser d'autres séparateurs.

Pour simplifier les opérations de lecture de textes constitués de lignes, la bibliothèque fournit la fonction getline. Cette fonction prend en premier paramètre le flux d'entrée sur lequel elle doit lire la ligne, et la basic_string dans laquelle elle doit stocker cette ligne en deuxième paramètre. Le troisième paramètre permet d'indiquer le caractère séparateur de ligne. Ce paramètre est facultatif, car il dispose d'une valeur par défaut qui correspond au caractère de fin de ligne classique '\n'.

Exemple 14-14. Lecture de lignes sur le flux d'entrée
Sélectionnez

#include <iostream>
#include <string>
 
using namespace std;
 
int main(void)
{
    string s1, s2;
    getline(cin, s1);
    getline(cin, s2);
    cout << "Première ligne : " << s1 << endl;
    cout << "Deuxième ligne : " << s2 << endl;
    return 0;
}
 

14.2. Les types utilitaires

La bibliothèque standard utilise en interne un certain nombre de types de données spécifiques, essentiellement dans un but de simplicité et de facilité d'écriture. Ces types seront en général rarement utilisés par les programmeurs, mais certaines fonctionnalités de la bibliothèque standard peuvent y avoir recours. Il faut donc connaître leur existence et savoir les manipuler correctement.

14.2.1. Les pointeurs auto

La plupart des variables détruisent leur contenu lorsqu'elles sont détruites elles-mêmes. Une exception notable à ce comportement est bien entendu celle des pointeurs, qui par définition ne contiennent pas eux-mêmes leurs données mais plutôt une référence sur celles-ci. Lorsque ces données sont allouées dynamiquement, il faut systématiquement penser à les détruire manuellement lorsqu'on n'en a plus besoin. Cela peut conduire à des fuites de mémoire (« Memory Leak » en anglais) très facilement. Si de telles fuites ne sont pas gênantes pour les processus dont la durée de vie est très courte, elles peuvent l'être considérablement plus pour les processus destinés à fonctionner longtemps, si ce n'est en permanence, sur une machine.

En fait, dans un certain nombre de cas, l'allocation dynamique de mémoire n'est utilisée que pour effectuer localement des opérations sur un nombre arbitraire de données qui ne peut être connu qu'à l'exécution. Cependant, il est relativement rare d'avoir à conserver ces données sur de longues périodes, et il est souvent souhaitable que ces données soient détruites lorsque la fonction qui les a allouées se termine. Autrement dit, il faudrait que les pointeurs détruisent automatiquement les données qu'ils référencent lorsqu'ils sont eux-mêmes détruits.

La bibliothèque standard C++ fournit à cet effet une classe d'encapsulation des pointeurs, qui permet d'obtenir ces fonctionnalités. Cette classe se nomme auto_ptr, en raison du fait que ses instances sont utilisées comme des pointeurs de données dont la portée est la même que celle des variables automatiques. La déclaration de cette classe est réalisée comme suit dans l'en-tête memory :

 
Sélectionnez

template <class T>
class auto_ptr
{
public:
    typedef T element_type;
    explicit auto_ptr(T *pointeur = 0) throw();
    auto_ptr(const auto_ptr &source) throw();
    template <class U>
    auto_ptr(const auto_ptr<U> &source) throw();
    ~auto_ptr() throw();
 
    auto_ptr &operator=(const auto_ptr &source) throw();
    template <class U>
    auto_ptr &operator=(const auto_ptr<U> &source) throw();
 
    T &operator*() const throw();
    T *operator->() const throw();
    T *get() const throw();
    T *release() const throw();
};
 

Cette classe permet de construire un objet contrôlant un pointeur sur un autre objet alloué dynamiquement avec l'opérateur new. Lorsqu'il est détruit, l'objet référencé est automatiquement détruit par un appel à l'opérateur delete. Cette classe utilise donc une sémantique de propriété stricte de l'objet contenu, puisque le pointeur ainsi contrôlé ne doit être détruit qu'une seule fois.

Cela implique plusieurs remarques. Premièrement, il y a nécessairement un transfert de propriété du pointeur encapsulé lors des opérations de copie et d'affectation. Deuxièmement, toute opération susceptible de provoquer la perte du pointeur encapsulé provoque sa destruction automatiquement. C'est notamment le cas lorsqu'une affectation d'une autre valeur est faite sur un auto_ptr contenant déjà un pointeur valide. Enfin, il ne faut jamais détruire soi-même l'objet pointé une fois que l'on a affecté un pointeur sur celui-ci à un auto_ptr.

Il est très simple d'utiliser les pointeurs automatiques. En effet, il suffit de les initialiser à leur construction avec la valeur du pointeur sur l'objet alloué dynamiquement. Dès lors, il est possible d'utiliser l'auto_ptr comme le pointeur original, puisqu'il définit les opérateurs '*' et '->'.

Les auto_ptr sont souvent utilisés en tant que variable automatique dans les sections de code susceptible de lancer des exceptions, puisque la remontée des exceptions détruit les variables automatiques. Il n'est donc plus nécessaire de traiter ces exceptions et de détruire manuellement les objets alloués dynamiquement avant de relancer l'exception.

Exemple 14-15. Utilisation des pointeurs automatiques
Sélectionnez

#include <iostream>
#include <memory>
 
using namespace std;
 
class A
{
public:
    A()
    {
        cout << "Constructeur" << endl;
    }
 
    ~A()
    {
        cout << "Destructeur" << endl;
    }
};
 
// Fonction susceptible de lancer une exception :
void f()
    // Alloue dynamiquement un objet :
    auto_ptr<A> p(new A);
    // Lance une exception, en laissant au pointeur
    // automatique le soin de détruire l'objet alloué :
    throw 2;
}
 
int main(void)
{
    try
    {
        f();
    }
    catch (...)
    {
    }
    return 0;
}

Note : On prendra bien garde au fait que la copie d'un auto_ptr dans un autre effectue un transfert de propriété. Cela peut provoquer des surprises, notamment si l'on utilise des paramètres de fonctions de type auto_ptr (chose expressément déconseillée). En effet, il y aura systématiquement transfert de propriété de l'objet lors de l'appel de la fonction, et c'est donc la fonction appelée qui en aura la responsabilité. Si elle ne fait aucun traitement spécial, l'objet sera détruit avec le paramètre de la fonction, lorsque l'exécution du programme en sortira ! Inutile de dire que la fonction appelante risque d'avoir des petits problèmes... Pour éviter ce genre de problèmes, il est plutôt conseillé de passer les auto_ptr par référence constante plutôt que par valeur dans les appels de fonctions.

Un autre piège classique est d'initialiser un auto_ptr avec l'adresse d'un objet qui n'a pas été alloué dynamiquement. Il est facile de faire cette confusion, car on ne peut a priori pas dire si un pointeur pointe sur un objet alloué dynamiquement ou non. Quoi qu'il en soit, si vous faites cette erreur, un appel à delete sera fait avec un paramètre incorrect lors de la destruction du pointeur automatique et le programme plantera.

Enfin, sachez que les pointeurs automatiques n'utilisent que l'opérateur delete pour détruire les objets qu'ils encapsulent, jamais l'opérateur delete[]. Par conséquent, les pointeurs automatiques ne devront jamais être initialisés avec des pointeurs obtenus lors d'une allocation dynamique avec l'opérateur new[] ou avec la fonction malloc de la bibliothèque C.

Il est possible de récupérer la valeur du pointeur pris en charge par un pointeur automatique simplement, grâce à la méthode get. Cela permet de travailler avec le pointeur original, cependant, il ne faut jamais oublier que c'est le pointeur automatique qui en a toujours la propriété. Il ne faut donc jamais appeler delete sur le pointeur obtenu.

En revanche, si l'on veut sortir le pointeur d'un auto_ptr, et forcer celui-ci à en abandonner la propriété, on peut utiliser la méthode release. Cette méthode renvoie elle-aussi le pointeur sur l'objet que l'auto_ptr contenait, mais libère également la référence sur l'objet pointé au sein de l'auto_ptr. Ainsi, la destruction du pointeur automatique ne provoquera plus la destruction de l'objet pointé et il faudra à nouveau prendre en charge cette destruction soi-même.

Exemple 14-16. Sortie d'un pointeur d'un auto_ptr
Sélectionnez

#include <iostream>
#include <memory>
 
using namespace std;
 
class A
{
public:
    A()
    {
        cout << "Constructeur" << endl;
    }
 
    ~A()
    {
        cout << "Destructeur" << endl;
    }
};
 
A *f(void)
{
    cout << "Construcion de l'objet" << endl;
    auto_ptr<A> p(new A);
    cout << "Extraction du pointeur" << endl;
    return p.release();
}
 
int main(void)
{
    A *pA = f();
    cout << "Destruction de l'objet" << endl;
    delete pA;
    return 0;
}
 

14.2.2. Les paires

Outre les pointeurs automatiques, la bibliothèque standard C++ définit une autre classe utilitaire qui permet quant à elle de stocker un couple de valeurs dans un même objet. Cette classe, la classe template pair, est en particulier très utilisée dans l'implémentation de certains conteneurs de la bibliothèque.

La déclaration de la classe template pair est la suivante dans l'en-tête utility :

 
Sélectionnez

template <class T1, class T2>
struct pair
{
    typedef T1 first_type;
    typedef T2 second_type;
 
    T1 first;
    T2 second;
 
    pair();
    pair(const T1 &, const T2 &);
    template <class U1, class U2>
    pair(const pair<U1, U2> &);
};
 
template <class T1, class T2>
bool operator==(const pair<T1, T2> &, const pair<T1, T2> &);
 
template <class T1, class T2>
bool operator<(const pair<T1, T2> &, const pair<T1, T2> &);
 
template <class T1, class T2>
pair<T1, T2> make_pair(const T1 &, const T2 &);
 

Comme cette déclaration le montre, l'utilisation de la classe pair est extrêmement simple. La construction d'une paire se fait soit en fournissant le couple de valeurs devant être stocké dans la paire, soit en appelant la fonction make_pair. La récupération des deux composantes d'une paire se fait simplement en accédant aux données membres publiques first et second.

Exemple 14-17. Utilisation des paires
Sélectionnez

#include <iostream>
#include <utility>
 
using namespace std;
 
int main(void)
{
    // Construit une paire associant un entier
    // à un flottant :
    pair<int, double> p1(5, 7.38), p2;
    // Initialise p2 avec make_pair :
    p2 = make_pair(9, 3.14);
    // Affiche les deux paires :
    cout << "p1 = (" << p1.first << ", "
        << p1.second << ")" << endl;
    cout << "p2 = (" << p2.first << ", "
        << p2.second << ")" << endl;
    return 0;
}

La classe template pair dispose également d'opérateurs de comparaison qui utilisent l'ordre lexicographique induit par les valeurs de ses deux éléments. Deux paires sont donc égales si et seulement si leurs couples de valeurs sont égaux membre à membre, et une paire est inférieure à l'autre si la première valeur de la première paire est inférieure à la valeur correspondante de la deuxième paire, ou, si elles sont égales, la deuxième valeur de la première paire est inférieure à la deuxième valeur de la deuxième paire.

14.3. Les types numériques

En plus des types d'intérêt général vus dans les sections précédentes, la bibliothèque standard fournit des types de données plus spécialisés dans les calculs numériques ou mathématiques. Ces types de données permettent d'effectuer des calculs sur les nombres complexes, ainsi que des calculs parallèles sur des tableaux de valeurs.

14.3.1. Les complexes

Les types de base du langage C++ fournissent une approximation relativement fiable des différents domaines de nombres mathématiques. Par exemple, le type int permet de représenter une plage de valeurs limitée des entiers relatifs, mais suffisamment large toutefois pour permettre d'effectuer la plupart des calculs intervenant dans la vie réelle. De même, les types des nombres à virgule flottante fournissent une approximation relativement satisfaisante des nombres réels des mathématiques. L'approximation cette fois porte non seulement sur la plage de valeur accessible, mais également sur la précision des nombres.

Note : On prendra bien conscience du fait que les types du langage ne représentent effectivement que des approximations, car les ordinateurs sont des machines limitées en mémoire et en capacité de représentation du monde réel. Il faut donc toujours penser aux éventuels cas de débordements et erreurs de représentation des nombres, surtout en ce qui concerne les nombres réels. Les bogues les plus graves (en terme de pertes matérielles ou humaines) sont souvent dûs à de tels débordements, qui sont inhérents aux techniques utilisées par l'informatique (même avec des langages plus « sûrs » que le C++).

Il existe en mathématiques un autre type de nombres, qui n'ont pas de représentation physique immédiate pour le commun des mortels, mais qui permettent souvent de simplifier beaucoup certains calculs : les nombres complexes. Ces nombres étendent en effet le domaine des nombres accessibles et permettent de poursuivre les calculs qui n'étaient pas réalisables avec les nombres réels seulement, en s'affranchissant des contraintes imposées sur les solutions des équations algébriques. Les nombres complexes sont donc d'une très grande utilité dans toute l'algèbre, et en particulier dans les calculs matriciels où ils prennent une place prédominante. Les nombres complexes permettent également de simplifier sérieusement les calculs trigonométriques, les calculs de signaux en électricité et les calculs en mécanique quantique. Le plus intéressant avec ces nombres est sans doute le fait que même si les résultats intermédiaires que l'on trouve avec eux n'ont pas de signification réelle, les résultats finaux, eux, peuvent en avoir une et n'auraient pas été trouvés aussi facilement en conservant toutes les contraintes imposées par les nombres réels.

Afin de simplifier la vie des programmeurs qui ont besoin de manipuler des nombres complexes, la bibliothèque standard C++ définit la classe template complex, qui permet de les représenter et d'effectuer les principales opérations mathématiques dessus. Si l'utilisation de la classe complex en soi ne pose aucun problème particulier, il peut être utile de donner une description sommaire de ce qu'est un nombre complexe pour les néophytes en mathématiques. Toutefois, cette description n'est pas destinée aux personnes n'ayant aucune connaissance en mathématiques (si tant est qu'un programmeur puisse être dans ce cas...). Si vous ne la comprenez pas, c'est sans doute que vous n'avez aucunement besoin des nombres complexes et vous pouvez donc passer cette section sans crainte.

14.3.1.1. Définition et principales propriétés des nombres complexes

Il n'est pas compliqué de se représenter ce que signifie un nombre réel puisqu'on les utilise couramment dans la vie courante. La méthode la plus simple est d'imaginer une règle graduée où chaque position est donnée par un nombre réel par rapport à l'origine. Ce nombre indique le nombre de fois que l'unité de distance doit être répétée depuis l'origine pour arriver à cette position.

Pour se représenter la valeur d'un nombre complexe, il faut utiliser une dimension supplémentaire. En fait, tout nombre complexe peut être exprimé avec deux valeurs réelles : la partie réelle du complexe, et sa partie imaginaire. Plusieurs notations existent pour représenter les nombres complexes à partir de ces deux parties. La plus courante est de donner la partie réelle et la partie imaginaire entre parenthèses, séparées par une virgule :

 
Sélectionnez

(réelle, imaginaire)

réelle est la valeur de la partie réelle, et imaginaire la valeur de la partie imaginaire. Il est également très courant en France de noter les deux parties directement en les séparant d'un signe d'addition et en accolant le caractère 'i' (pour « imaginaire ») à la partie imaginaire :

 
Sélectionnez

réelle + imaginaire i
 

L'exemple suivant vous présente quelques nombres complexes :

 
Sélectionnez

7.56
3i
5+7i
 

Vous constaterez que les nombres réels peuvent parfaitement être représentés par les nombres complexes, puisqu'il suffit simplement d'utiliser une partie imaginaire nulle.

Les opérations algébriques classiques ont été définies sur les nombres complexes. Les additions et soustractions se font membre à membre, partie réelle avec partie réelle et partie imaginaire avec partie imaginaire. En revanche, la multiplication est un peu plus complexe, car elle se base sur la propriété fondamentale que le carré de l'unité de la partie imaginaire vaut -1. Autrement dit, le symbole i de la notation précédente dispose de la propriété fondamentale suivante : i²=-1. Il s'agit en quelque sorte d'une racine carrée de -1 (la racine carrée des nombres négatifs n'ayant pas de sens, puisqu'un carré est normalement toujours positif, on comprend la qualification d'« imaginaire » des nombres complexes). À partir de cette règle de base, et en conservant les règles d'associativité des opérateurs, on peut définir le produit de deux nombres complexes comme suit :

 
Sélectionnez

(a,b) * (c,d) = (ac - bd, ad + bc)
 

Enfin, la division se définit toujours comme l'opération inverse de la multiplication, c'est-à-dire l'opération qui trouve le nombre qui, multiplié par le diviseur, redonne le dividende. Chaque nombre complexe non nul dispose d'un inverse, qui est le résultat de la division de 1 par ce nombre. On peut montrer facilement que l'inverse d'un nombre complexe est défini comme suit :

 
Sélectionnez

1/(a,b) = (a / (a&#178; + b&#178;), -b / (a&#178; + b&#178;))
 

À partir de l'inverse, il est simple de calculer une division quelconque.

Comme il l'a été dit plus haut, les nombres complexes peuvent être représentés en utilisant une dimension supplémentaire. Ainsi, si on définit un repère dans le plan, dont l'axe des abscisses est associé à la partie réelle des nombres complexes et l'axe des ordonnées à la partie imaginaire, à tout nombre complexe est associé un point du plan. On appelle alors ce plan le plan complexe. La définition des complexes donnée ici correspond donc à un système de coordonnées cartésiennes du plan complexe, et chaque nombre complexe dispose de ses propres coordonnées.

En mathématiques, il est également courant d'utiliser un autre système de coordonnées : le système de coordonnées polaires. Dans ce système, chaque point du plan est identifié non plus par les coordonnées de ses projections orthogonales sur les axes du repère, mais par sa distance à l'origine et par l'angle que la droite qui rejoint l'origine au point fait avec l'axe des abscisses. Ces deux nombres sont couramment notés respectivement avec les lettres grecques rho et theta. La dénomination de coordonnées polaires provient du fait que l'origine du repère joue le rôle d'un pôle par rapport auquel on situe le point dans le plan.

Il est donc évident que les nombres complexes peuvent également être représentés par leurs coordonnées polaires. On appelle généralement la distance à l'origine la norme du nombre complexe, et l'angle qu'il fait avec l'axe des abscisses son argument. Faites bien attention à ce terme, il ne représente pas un argument d'une fonction ou quoi que ce soit qui se rapporte à la programmation.

La plupart des fonctions mathématiques classiques ont été définies sur les nombres complexes, parfois en restreignant leur domaine de validité. Ainsi, il est possible de calculer un sinus, un cosinus, une exponentielle, etc. pour les nombres complexes. Il est bien entendu hors de question de définir rigoureusement, ni même de présenter succinctement ces fonctions dans ce document. Cependant, il est bon de savoir qu'on ne peut pas définir une relation d'ordre sur les nombres complexes. Autrement dit, on ne peut pas faire d'autre comparaison que l'égalité entre deux nombres complexes (essayez de comparer les nombres complexes situés sur un cercle centré à l'origine dans le plan complexe pour vous en rendre compte).

14.3.1.2. La classe complex

La classe template complex est définie dans l'en-tête complex de la bibliothèque standard. Cette classe peut être instanciée pour l'un quelconque des trois types de nombre à virgule flottante du langage : float, double ou long double. Elle permet d'effectuer les principales opérations définies sur les nombres complexes, comme les additions, soustractions, multiplications, division, mais également des opérations spécifiques aux nombres complexes, comme la détermination de leur argument ou de leur norme. Enfin, l'en-tête complex contient des surcharges des fonctions mathématiques standards, telles que les fonctions trigonométriques, la racine carrée, les puissances et exponentielles, ainsi que les logarithmes (définis sur le plan complexe auquel l'axe des abscisses négatives a été ôté).

La construction d'un complexe ne pose aucun problème en soi. La classe complex dispose d'un constructeur par défaut, d'un constructeur de copie et d'un constructeur prenant en paramètre la partie réelle et la partie imaginaire du nombre :

 
Sélectionnez

 #include <iostream>
#include <complex>
 
using namespace std;
 
int main(void)
{
    complex<double> c(2,3);
    cout << c << endl;
    return 0;
}
 

L'exemple précédent présente également l'opérateur de sortie sur les flux standards, qui formate un nombre complexe en utilisant la notation (réel,imaginaire). Il existe également une surcharge de l'opérateur d'entrée pour le flux d'entrée :

 
Sélectionnez

#include <iostream>
#include <complex>
 
using namespace std;
 
int main(void)
{
    complex<double> c;
    cin >> c;
    cout << "Vous avez saisi : " << c << endl;
    return 0;
}
 

Note : Malheureusement, cette notation pose des problèmes avec la locale française, puisque nous utilisons des virgules pour séparer la partie entière de la partie décimale des nombres à virgules. Lorsque l'un des deux nombres flottants est un entier, il est impossible de déterminer où se trouve la virgule séparant la partie entière de la partie imaginaire du nombre complexe. Une première solution est de modifier le formatage des nombres réels pour que les chiffres après la virgule soient toujours affichés, même s'ils sont nuls. Cependant, il faut également imposer que les saisies des nombres soient également toujours effectués avec des nombres à virgules, ce qui est sujet à erreur et invérifiable. Il est donc recommandé de n'utiliser que la locale de la bibliothèque C lorsqu'on fait un programme utilisant les nombres complexes.

Il n'existe pas de constructeur permettant de créer un nombre complexe à partir de ses coordonnées polaires. En revanche, la fonction polar permet d'en construire un. Cette fonction prend en paramètre la norme du complexe à construire ainsi que son argument. Elle renvoie le nombre complexe nouvellement construit.

La partie imaginaire et la partie réelle d'un nombre complexe peuvent être récupérées à tout instant à l'aide des méthodes real et imag de la classe template complex. Il est également possible d'utiliser les fonctions template real et imag, qui prennent toutes deux le nombre complexe dont il faut calculer la partie réelle et la partie imaginaire. De même, la norme d'un nombre complexe est retournée par la fonction abs, et son argument peut être obtenu avec la fonction arg.

Bien entendu, les opérations classiques sur les complexes se font directement, comme s'il s'agissait d'un type prédéfini du langage :

 
Sélectionnez

#include <iostream>
#include <complex>
 
using namespace std;
 
int main(void)
{
    complex<double> c1(2.23, 3.56);
    complex<double> c2(5, 5);
    complex<double> c = c1+c2;
    c = c/(c1-c2);
    cout << c << endl;
    return 0;
}
 

Les fonctions spécifiques permettant de manipuler les complexes et de leur appliquer les opérations qui leurs sont propres sont récapitulées dans le tableau suivant :

Tableau 14-2. Fonctions spécifiques aux complexes

Fonction Description
real Retourne la partie réelle du nombre complexe.
imag Retourne la partie imaginaire du nombre complexe.
abs Retourne la norme du nombre nombre complexe, c'est-à-dire sa distance à l'origine.
arg Retourne l'argument du nombre complexe.
norm Retourne le carré de la norme du nombre complexe. Attention, cette fonction porte mal son nom, puisque la vraie norme est retournée par la surcharge de la fonction abs pour les nombres complexes. Cette incohérence provient de l'interprétation différente de celle des Français que font les Anglo-saxons de la notion de norme.
conj Retourne le nombre complexe conjugué du nombre complexe fourni en argument. Le nombre conjugué d'un nombre complexe est son symétrique par rapport à l'axe des abscisses dans le plan complexe, c'est-à-dire qu'il dispose de la même partie réelle, mais que sa partie imaginaire est opposée à celle du nombre complexe original (cela revient également à dire que l'argument du conjugué est l'opposé de l'argument du complexe original). Le produit d'un nombre complexe et de son conjugué donne le carré de sa norme.
polar Permet de construire un nombre complexe à partir de ses coordonnées polaires.
Exemple 14-18. Manipulation des nombres complexes
Sélectionnez

#include <iostream>
#include <complex>
 
using namespace std;
 
int main(void)
{
    // Crée un nombre complexe :
    complex<double> c(2,3);
    // Détermine son argument et sa norme :
    double Arg = arg(c);
    double Norm = abs(c);
    // Construit le nombre complexe conjugué :
    complex<double> co = polar(Norm, -Arg);
    // Affiche le carré de la norme du conjugué :
    cout << norm(co) << endl;
    // Calcule le carré ce cette norme par le produit
    // du complexe et de son conjugué :
    cout << real(c * conj(c)) << endl;
    return 0;
}

14.3.2. Les tableaux de valeurs

Comme il l'a été expliqué dans le Chapitre 1, les programmes classiques fonctionnent toujours sur le même principe : ils travaillent sur des données qu'ils reçoivent en entrée et produisent des résultats en sortie. Ce mode de fonctionnement convient dans la grande majorité des cas, et en fait les programmes que l'on appelle couramment les « filtres » en sont une des applications principales. Un filtre n'est rien d'autre qu'un programme permettant, comme son nom l'indique, de filtrer les données reçues en entrée selon un critère particulier et de ne fournir en sortie que les données qui satisfont ce critère. Certains filtres plus évolués peuvent même modifier les données à la volée ou les traduire dans un autre format. Les filtres sont très souvent utilisés avec les mécanismes de redirection des systèmes qui les supportent afin d'exécuter des traitements complexes sur les flux de données à partir de filtres simples, en injectant les résultats des uns dans le flux d'entrée des autres.

Cependant, ce modèle a une limite pratique en terme de performances, car il nécessite un traitement séquentiel des données. La vitesse d'exécution d'un programme conçu selon ce modèle est donc directement lié à la vitesse d'exécution des instructions, donc à la vitesse du processeur de la machine utilisée. Lorsqu'un haut niveau de performance doit être atteint, plusieurs solutions sont disponibles. Dans la pratique, on distingue trois solutions classiques.

La première solution consiste simplement, pour augmenter la puissance d'une machine, à augmenter celle du processeur. Cela se traduit souvent par une augmentation de la fréquence de ce processeur, technique que tout le monde connaît. Les avantages de cette solution sont évidents : tous les programmes bénéficient directement de l'augmentation de la puissance du processeur et n'ont pas à être modifiés. En revanche, cette technique atteindra un jour ou un autre ses limites en termes de coûts de fabrication et de moyens techniques à mettre en oeuvre pour produire les processeurs.

La deuxième solution est d'augmenter le nombre de processeurs de la machine. Cette solution est très simple, mais suppose que les programmes soient capables d'effectuer plusieurs calculs indépendants simultanément. En particulier, les traitements à effectuer doivent être suffisamment indépendants et ne pas à avoir à attendre les données produites par les autres afin de pouvoir réellement être exécutés en parallèle. On quitte donc le modèle séquentiel, pour entrer dans un modèle de traitement où chaque processeur travaille en parallèle (modèle « MIMD », abréviation de l'anglais « Multiple Instruction Multiple Data »). Cette technique est également souvent appelée le parallélisme de traitement. Malheureusement, pour un unique processus purement séquentiel, cette technique ne convient pas, puisque de toutes façons, les opérations à exécuter ne le seront que par un seul processeur.

Enfin, il existe une technique mixte, qui consiste à paralléliser les données. Les mêmes opérations d'un programme séquentiel sont alors exécutées sur un grand nombre de données similaires. Les données sont donc traitées par blocs, par un unique algorithme : il s'agit du parallélisme de données (« SIMD » en anglais, abréviation de « Single Instruction Multiple Data »). Cette solution est celle mise en oeuvre dans les processeurs modernes qui disposent de jeux d'instructions spécialisées permettant d'effectuer des calculs sur plusieurs données simultanément (MMX, 3DNow et SSE pour les processeurs de type x86 par exemple). Bien entendu, cette technique suppose que le programme ait effectivement à traiter des données semblables de manière similaire. Cette contrainte peut paraître très forte, mais, en pratique, les situations les plus consommatrices de ressources sont justement celles qui nécessite la répétition d'un même calcul sur plusieurs données. On citera par exemple tous les algorithmes de traitement de données multimédia, dont les algorithmes de compression, de transformation et de combinaison.

Si l'augmentation des performances des processeurs apporte un gain directement observable sur tous les programmes, ce n'est pas le cas pour les techniques de parallélisation. Le parallélisme de traitement est généralement accessible au niveau système, par l'intermédiaire du multitâche et de la programmation multithreadée. Il faut donc écrire les programmes de telle sorte à bénéficier de ce parallélisme de traitement, à l'aide des fonctions spécifique au système d'exploitation. De même, le parallélisme de données nécessite la définition de types de données complexes, capables de représenter les blocs de données sur lesquels le programme doit travailler. Ces blocs de données sont couramment gérés comme des vecteurs ou des matrices, c'est-à-dire, en général, comme des tableaux de nombres. Le programme doit donc utiliser ces types spécifiques pour accéder à toutes les ressources de la machine. Cela nécessite un support de la part du langage de programmation.

Chaque environnement de développement est susceptible de fournir les types de données permettant d'effectuer des traitements SIMD. Cependant, ces types dépendent de l'environnement utilisé et encore plus de la plate-forme utilisée. La bibliothèque standard C++ permet d'éviter ces écueils, car elle définit un type de donnée permettant de traiter des tableaux unidimensionnels d'objets, en assurant que les mécanismes d'optimisation propre aux plates-formes matérielles et aux compilateurs seront effectivement utilisés : les valarray.

14.3.2.1. Fonctionnalités de base des valarray

La classe valarray est une classe template capable de stocker un tableau de valeurs de son type template. Il est possible de l'instancier pour tous les types de données pour lesquels les opérations définies sur la classe valarray sont elles-mêmes définies. La bibliothèque standard C++ garantit que la classe valarray est écrite de telle sorte que tous les mécanismes d'optimisation des compilateurs pourront être appliqués sur elle, afin d'obtenir des performances optimales. De plus, chaque implémentation est libre d'utiliser les possibilités de calcul parallèle disponible sur chaque plate-forme, du moins pour les types pour lesquels ces fonctionnalités sont présentes. Par exemple, la classe valarray instanciée pour le type float peut utiliser les instructions spécifiques de calcul sur les nombres flottants du processeur si elles sont disponibles. Toutefois, la norme n'impose aucune contrainte à ce niveau, et la manière dont la classe valarray est implémentée reste à la discrétion de chaque fournisseur.

La classe valarray fournit toutes les fonctionnalités nécessaires à la construction des tableaux de valeurs, à leur initialisation, ainsi qu'à leur manipulation. Elle est déclarée comme suit dans l'en-tête valarray :

 
Sélectionnez

// Déclaration des classes de sélection de sous-tableau :
class slice;
class gslice;
 
// Déclaration de la classe valarray :
template <class T>
class valarray
{
public:
    // Types des données :
    typedef T value_type;
 
    // Constructeurs et destructeurs :
    valarray();
    explicit valarray(size_t taille);
    valarray(const T &valeur, size_t taille);
    valarray(const T *tableau, size_t taille);
    valarray(const valarray &source);
    valarray(const mask_array<T> &source);
    valarray(const indirect_array<T> &source);
    valarray(const slice_array<T> &source);
    valarray(const gslice_array<T> &source);
    ~valarray();
 
    // Opérateurs d'affectation :
    valarray<T> &operator=(const T &valeur);
    valarray<T> &operator=(const valarray<T> &source);
    valarray<T> &operator=(const mask_array<T> &source);
    valarray<T> &operator=(const indirect_array<T> &source);
    valarray<T> &operator=(const slice_array<T> &source);
    valarray<T> &operator=(const gslice_array<T> &source);
 
    // Opérateurs d'accès aux éléments :
    T operator[](size_t indice) const;
    T &operator[](size_t indice);
 
    // Opérateurs de sélection de sous-ensemble du tableau :
    valarray<T>       operator[](const valarray<bool> &masque) const;
    mask_array<T>     operator[](const valarray<bool> &masque);
    valarray<T>       operator[](const valarray<size_t> &indices) const;
    indirect_array<T> operator[](const valarray<size_t> &indices);
    valarray<T>       operator[](slice selecteur) const;
    slice_array<T>    operator[](slice selecteur);
    valarray<T>       operator[](const gslice &selecteur) const;
    gslice_array<T>   operator[](const gslice &selecteur);
 
    // Opérateurs unaires :
    valarray<T> operator+() const;
    valarray<T> operator-() const;
    valarray<T> operator~() const;
    valarray<T> operator!() const;
 
    // Opérateurs d'affectation composée :
    valarray<T> &operator*=(const T &valeur);
    valarray<T> &operator*=(const valarray<T> &tableau);
    valarray<T> &operator/=(const T &valeur);
    valarray<T> &operator/=(const valarray<T> &tableau);
    valarray<T> &operator%=(const T &valeur);
    valarray<T> &operator%=(const valarray<T> &tableau);
    valarray<T> &operator+=(const T &valeur);
    valarray<T> &operator+=(const valarray<T> &tableau);
    valarray<T> &operator-=(const T &valeur);
    valarray<T> &operator-=(const valarray<T> &tableau);
    valarray<T> &operator^=(const T &valeur);
    valarray<T> &operator^=(const valarray<T> &tableau);
    valarray<T> &operator&=(const T &valeur);
    valarray<T> &operator&=(const valarray<T> &tableau);
    valarray<T> &operator|=(const T &valeur);
    valarray<T> &operator|=(const valarray<T> &tableau);
    valarray<T> &operator<<=(const T &valeur);
    valarray<T> &operator<<=(const valarray<T> &tableau);
    valarray<T> &operator>>=(const T &valeur);
    valarray<T> &operator>>=(const valarray<T> &tableau);
 
    // Opérations spécifiques :
    size_t size() const;
    T sum() const;
    T min() const;
    T max() const;
    valarray<T> shift(int) const;
    valarray<T> cshift(int) const;
    valarray<T> apply(T fonction(T)) const;
    valarray<T> apply(T fonction(const T &)) const;
    void resize(size_t taille, T initial=T());
};

Nous verrons dans la section suivante la signification des types slice, gslice, slice_array, gslice_array, mask_array et indirect_array.

Il existe plusieurs constructeurs permettant de créer et d'initialiser un tableau de valeurs. Le constructeur par défaut initialise un tableau de valeur vide. Les autres constructeurs permettent d'initialiser le tableau de valeur à partir d'une valeur d'initialisation pour tous les éléments du valarray, ou d'un autre tableau contenant les données à affecter aux éléments du valarray :

 
Sélectionnez

// Construit un valarray de doubles :
valarray<double> v1;
 
// Initialise un valarray de doubles explicitement :
double valeurs[] = {1.2, 3.14, 2.78, 1.414, 1.732};
valarray<double> v2(valeurs,
    sizeof(valeurs) / sizeof(double));
 
// Construit un valarray de 10 entiers initialisés à 3 :
valarray<int> v3(3, 10);

Vous pouvez constater que le deuxième argument des constructeurs qui permettent d'initialiser les valarray prennent un argument de type size_t, qui indique la taille du valarray. Une fois un valarray construit, il est possible de le redimensionner à l'aide de la méthode resize. Cette méthode prend en premier paramètre la nouvelle taille du valarray et la valeur à utiliser pour réinitialiser tous les éléments du valarray après redimensionnement. La valeur par défaut est celle fournie par le constructeur par défaut du type des données contenues dans le valarray. La taille courante d'un valarray peut être récupérée à tout moment grâce à la méthode size.

Exemple 14-19. Modification de la taille d'un valarray
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
int main(void)
{
    // Création d'un valarray :
    valarray<double> v;
    cout << v.size() << endl;
    // Redimensionnement du valarray :
    v.resize(5, 3.14);
    cout << v.size() << endl;
    return 0;
}

Toutes les opérations classiques des mathématiques peuvent être appliquées sur un valarray pourvu qu'elles puissent l'être également sur le type des données contenues par ce tableau. La définition de ces opérations est très simple : l'opération du type de base est appliquée simplement à chaque élément contenu dans le tableau de valeurs.

La bibliothèque standard définit également les opérateurs binaires nécessaires pour effectuer les opérations binaires sur chaque élément des valarray. En fait, ces opérateurs sont classés en deux catégories, selon la nature de leurs arguments. Les opérateurs de la première catégorie permettent d'effectuer une opération entre deux valarray de même dimension, en appliquant cette opération membre à membre. Il s'agit donc réellement d'une opération vectorielle dans ce cas. En revanche, les opérateurs de la deuxième catégorie appliquent l'opération avec une même et unique valeur pour chaque donnée stockée dans le valarray.

Exemple 14-20. Opérations sur les valarray
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
void affiche(const valarray<double> &v)
{
    size_t i;
    for (i=0; i<v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
 
int main(void)
{
    // Construit deux valarray de doubles :
    double v1[] = {1.1, 2.2, 3.3};
    double v2[] = {5.3, 4.4, 3.5};
    valarray<double> vect1(v1, 3);
    valarray<double> vect2(v2, 3);
    valarray<double> res(3);
    // Effectue une somme membre à membre :
    res = vect1 + vect2;
    affiche(res);
    // Calcule le sinus des membres du premier valarray :
    res = sin(vect1);
    affiche(res);
    return 0;
}
 

Parmi les opérateurs binaires que l'on peut appliquer à un valarray, on trouve bien entendu les opérateurs de comparaison. Ces opérateurs, contrairement aux opérateurs de comparaison habituels, ne renvoient pas un booléen, mais plutôt un autre tableau de booléens. En effet, la comparaison de deux valarray a pour résultat le valarray des résultats des comparaisons membres à membres des deux valarray.

La classe valarray dispose de méthodes permettant d'effectuer diverses opérations spécifiques aux tableaux de valeurs. La méthode sum permet d'obtenir la somme de toutes les valeurs stockées dans le tableau de valeur. Les méthodes shift et cshift permettent, quant à elles, de construire un nouveau valarray dont les éléments sont les éléments du valarray auquel la méthode est appliquée, décalés ou permutés circulairement d'un certain nombre de positions. Le nombre de déplacements effectués est passé en paramètre à ces deux fonctions, les valeurs positives entraînant des déplacements vers la gauche et les valeurs négatives des déplacements vers la droite. Dans le cas des décalages les nouveaux éléments introduits pour remplacer ceux qui n'ont pas eux-mêmes de remplaçant prennent la valeur spécifiée par le constructeur par défaut du type utilisé.

Exemple 14-21. Décalages et rotations de valeurs
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
void affiche(const valarray<double> &v)
{
    size_t i;
    for (i=0; i<v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
 
int main(void)
{
    // Construit un valarray de doubles :
    double v1[] = {1.1, 2.2, 3.3, 4.4, 5.5};
    valarray<double> vect1(v1, 5);
    valarray<double> res(5);
    // Effectue un décalage à gauche de deux positions :
    res = vect1.shift(2);
    affiche(res);
    // Effectue une rotation de 2 positions vers la droite :
    res = vect1.cshift(-2);
    affiche(res);
    return 0;
}
 

Enfin, il existe deux méthodes apply permettant d'appliquer une fonction à chaque élément d'un valarray et de construire un nouveau valarray de même taille et contenant les résultats. Ces deux surcharges peuvent travailler respectivement avec des fonctions prenant en paramètre soit par valeur, soit par référence, l'objet sur lequel elles doivent être appliquées.

14.3.2.2. Sélection multiple des éléments d'un valarray

Les éléments d'un valarray peuvent être accédés à l'aide de l'opérateur d'accès aux éléments de tableau '[]'. La fonction affiche des exemples du paragraphe précédent utilise cette fonctionnalité pour en récupérer la valeur. Cependant, les valarray dispose de mécanismes plus sophistiqués pour manipuler les éléments des tableaux de valeur en groupe, afin de bénéficier de tous les mécanismes d'optimisation qui peuvent exister sur une plate-forme donnée. Grâce à ces mécanismes, il est possible d'effectuer des opérations sur des parties seulement d'un valarray ou d'écrire de nouvelles valeurs dans certains de ses éléments seulement.

Pour effectuer ces sélections multiples, plusieurs techniques sont disponibles. Cependant, toutes ces techniques se basent sur le même principe, puisqu'elles permettent de filtrer les éléments du valarray pour n'en sélectionner qu'une partie seulement. Le résultat de ce filtrage peut être un nouveau valarray ou une autre classe pouvant être manipulée exactement de la même manière qu'un valarray.

En pratique, il existe quatre manières de sélectionner des éléments dans un tableau. Nous allons les détailler dans les sections suivantes.

14.3.2.2.1. Sélection par un masque

La manière la plus simple est d'utiliser un masque de booléens indiquant quels éléments doivent être sélectionnés ou non. Le masque de booléens doit obligatoirement être un valarray de même dimension que le valarray contenant les éléments à sélectionner. Chaque élément est donc sélectionné en fonction de la valeur du booléen correspondant dans le masque.

Une fois le masque construit, la sélection des éléments peut être réalisée simplement en fournissant ce masque à l'opérateur [] du valarray contenant les éléments à sélectionner. La valeur retournée par cet opérateur est alors une instance de la classe template mask_array, par l'intermédiaire de laquelle les éléments sélectionnés peuvent être manipulés. Pour les valarray constants cependant, la valeur retournée est un autre valarray, contenant une copie des éléments sélectionnés.

La classe mask_array fournit un nombre limité d'opérations. En fait, ses instances ne doivent être utilisées que pour effectuer des opérations simples sur les éléments du tableau sélectionné par le masque fourni à l'opérateur []. Les opérations réalisables seront décrites dans la Section 14.3.2.2.4.

La sélection des éléments d'un tableau par l'intermédiaire d'un masque est utilisée couramment avec les opérateurs de comparaison des valarray, puisque ceux-ci renvoient justement un tel masque. Il est donc très facile d'effectuer des opérations sur les éléments d'un valarray qui vérifient une certaine condition.

Exemple 14-22. Sélection des éléments d'un valarray par un masque
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
void affiche(const valarray<int> &v)
{
    size_t i;
    for (i=0; i<v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
 
int main(void)
{
    // Construit un valarray d'entier :
    int valeurs[] = { 1, 5, 9, 4, 3, 7, 21, 32 };
    valarray<int> vi(valeurs,
        sizeof(valeurs) / sizeof(int));
    affiche(vi);
    // Multiplie par 2 tous les multiples de 3 :
    vi[(vi % 3)==0] *= valarray<int>(2, vi.size());
    affiche(vi);
    return 0;
}						
 
14.3.2.2.2. Sélection par indexation explicite

La sélection des éléments d'un valarray par un masque de booléens est explicite et facile à utiliser, mais elle souffre de plusieurs défauts. Premièrement, il faut fournir un tableau de booléen de même dimension que le valarray source. Autrement dit, il faut fournir une valeur booléenne pour tous les éléments du tableau, même pour ceux qui ne nous intéressent pas. Ensuite, les éléments sélectionnés apparaissent systématiquement dans le même ordre que celui qu'ils ont dans le valarray source.

La bibliothèque standard C++ fournit donc un autre mécanisme de sélection, toujours explicite, mais qui permet de faire une réindexation des éléments ainsi sélectionnés. Cette fois, il ne faut plus fournir un masque à l'opérateur [], mais un valarray contenant directement les indices des éléments sélectionnés. Ces indices peuvent ne pas être dans l'ordre croissant, ce qui permet donc de réarranger l'ordre des éléments ainsi sélectionnés.

Exemple 14-23. Sélection des éléments d'un valarray par indexation
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
void affiche(const valarray<int> &v)
{
    size_t i;
    for (i=0; i<v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
 
int main(void)
{
    // Construit un valarray d'entier :
    int valeurs[] = { 1, 5, 9, 4, 3, 7, 21, 32 };
    valarray<int> vi(valeurs,
        sizeof(valeurs) / sizeof(int));
    affiche(vi);
    // Multiplie par 2 les éléments d'indices 2, 5 et 7 :
    size_t indices[] = {2, 5, 7};
    valarray<size_t> ind(indices,
        sizeof(indices) / sizeof(size_t));
    vi[ind] *= valarray<int>(2, ind.size());
    affiche(vi);
    return 0;
}

La valeur retournée par l'opérateur de sélection sur les valarray non constants est cette fois du type indirect_array. Comme pour la classe mask_array, les opérations réalisables par l'intermédiaire de cette classe sont limitées et doivent servir uniquement à modifier les éléments sélectionnés dans le valarray source.

14.3.2.2.3. Sélection par indexation implicite

Dans beaucoup de situations, les indices des éléments sélectionnés suivent un motif régulier et il n'est pas toujours pratique de spécifier ce motif explicitement. La méthode de sélection précédente n'est dans ce cas pas très pratique et il est alors préférable de sélectionner les éléments par un jeu d'indices décrits de manière implicite. La bibliothèque fournit à cet effet deux classes utilitaires permettant de décrire des jeux d'indices plus ou moins complexes : la classe slice et la classe gslice.

Ces deux classes définissent les indices des éléments à sélectionner à l'aide de plusieurs variables pouvant prendre un certain nombre de valeurs espacées par un pas d'incrémentation fixe. La définition des indices consiste donc simplement à donner la valeur de départ de l'indice de sélection, le nombre de valeurs à générer pour chaque variable et le pas qui sépare ces valeurs. Les variables de contrôle commencent toutes leur itération à partir de la valeur nulle et prennent comme valeurs successives les multiples du pas qu'elles utilisent.

Note : En réalité, la classe slice est un cas particulier de la classe gslice qui n'utilise qu'une seule variable de contrôle pour définir les indices. Les slice ne sont donc rien d'autre que des gslice unidimensionnels.

Le terme de gslice provient de l'anglais « Generalized Slice », qui signifie bien que les gslice sont des slice étendues à plusieurs dimensions.

La classe slice est relativement facile à utiliser, puisqu'il suffit de spécifier la valeur de départ de l'indice, le nombre de valeurs à générer et le pas qui doit les séparer. Elle est déclarée comme suit dans l'en-tête valarray :

 
Sélectionnez

class slice
{
public:
    slice();
    slice(size_t debut, size_t nombre, size_t pas);
 
    // Accesseurs :
    size_t start() const;
    size_t size() const;
    size_t stride() const;
};
 
Exemple 14-24. Sélection par indexation implicite
Sélectionnez

#include <iostream>
#include <valarray>
 
using namespace std;
 
void affiche(const valarray<int> &v)
{
    size_t i;
    for (i=0; i<v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
 
int main(void)
{
    // Construit un valarray d'entier :
    int valeurs[] = { 1, 5, 9, 4, 3, 7, 21, 32 };
    valarray<int> vi(valeurs, 8);
    affiche(vi);
    // Multiplie par 2 un élément sur 3 à partir du deuxième :
    slice sel(1, 3, 3);
    vi[sel] *= valarray<int>(2, vi.size());
    affiche(vi);
    // Multiplie par 2 un élément sur 3 à partir du deuxième :
    slice sel(1, 3, 3);
    vi[sel] *= valarray<int>(2, vi.size());
    affiche(vi);
    return 0;
}
 

La classe gslice est en revanche un peu plus difficile d'emploi puisqu'il faut donner le nombre de valeurs et le pas pour chaque variable de contrôle. Le constructeur utilisé prend donc en deuxième et troisième paramètres non plus deux valeurs de type size_t, mais deux valarray de size_t. La déclaration de la classe gslice est donc la suivante :

 
Sélectionnez

class gslice
{
public:
    gslice();
    gslice(size_t debut,
        const valarray<size_t> nombres,
        const valarray<size_t> pas);
 
    // Accesseurs :
    size_t start() const;
    valarray<size_t> size() const;
    valarray<size_t> stride() const;
};
 

Les deux valarray déterminant le nombre de valeurs des variables de contrôle et leurs pas doivent bien entendu avoir la même taille. L'ordre dans lequel les indices des éléments sélectionnés sont générés par la classe gslice est celui obtenu en faisant varier en premier les dernières variables caractérisées par les valarray fournis lors de sa construction. Par exemple, une classe gslice utilisant trois variables prenant respectivement 2, 3 et 5 valeurs et variant respectivement par pas de 3, 1 et 2 unités, en partant de l'indice 2, générera les indices suivants :

 
Sélectionnez

2, 4, 6, 8, 10,
3, 5, 7, 9, 11,
4, 6, 8, 10, 12,
 
5, 7, 9, 11, 13,
6, 8, 10, 12, 14,
7, 9, 11, 13, 15

La variable prenant cinq valeurs et variant de deux en deux est donc celle qui évolue le plus vite.

Comme vous pouvez le constater avec l'exemple précédent, un même indice peut apparaître plusieurs fois dans la série définie par une classe gslice. La bibliothèque standard C++ n'effectue aucun contrôle à ce niveau : il est donc du ressort du programmeur de bien faire attention à ce qu'il fait lorsqu'il manipule des jeux d'indices dégénérés.

Comme pour les autres techniques de sélection, la sélection d'éléments d'un valarray non constant par l'intermédiaire des classes slice et gslice retourne une instance d'une classe particulière permettant de prendre en charge les opérations de modification des éléments ainsi sélectionnés. Pour les sélections simples réalisées avec la classe slice, l'objet retourné est de type slice_array. Pour les sélections réalisées avec la classe gslice, le type utilisé est le type gslice_array.

14.3.2.2.4. Opérations réalisables sur les sélections multiples

Comme on l'a vu dans les sections précédentes, les sélections multiples réalisées sur des objets non constants retournent des instances des classes utilitaires mask_array, indexed_array, slice_array et gslice_array. Ces classes référencent les éléments ainsi sélectionnés dans le valarray source, permettant ainsi de les manipuler en groupe. Cependant, ce ne sont pas des valarray complets et, en fait, ils ne doivent être utilisés, de manière générale, que pour effectuer une opération d'affectation sur les éléments sélectionnés. Ces classes utilisent donc une interface restreinte de celle de la classe valarray, qui n'accepte que les opérateurs d'affectation sur les éléments qu'elles représentent.

Par exemple, la classe mask_array est déclarée comme suit dans l'en-tête valarray :

 
Sélectionnez

template <class T>
class mask_array
{
public:
    typedef T value_type;
    ~mask_array();
 
    // Opérateurs d'affectation et d'affectation composées :
    void operator=(const valarray<T> &) const;
    void operator*=(const valarray<T> &) const;
    void operator/=(const valarray<T> &) const;
    void operator%=(const valarray<T> &) const;
    void operator+=(const valarray<T> &) const;
    void operator-=(const valarray<T> &) const;
    void operator^=(const valarray<T> &) const;
    void operator&=(const valarray<T> &) const;
    void operator|=(const valarray<T> &) const;
    void operator<<=(const valarray<T> &) const;
    void operator>>=(const valarray<T> &) const;
    void operator=(const T &valeur);
};
 

Tous ces opérateurs permettent d'affecter aux éléments de la sélection représentés par cette classe les valeurs spécifiées par leur paramètre. En général, ces valeurs doivent être fournies sous la forme d'un valarray, mais il existe également une surcharge de l'opérateur d'affectation permettant de leur affecter à tous une même valeur.

Note : Les sélections réalisées sur les valarray constants ne permettent bien entendu pas de modifier leurs éléments. Les objets retournés par l'opérateur [] lors des sélections multiples sur ces objets sont donc des valarray classiques contenant une copie des valeurs des éléments sélectionnés.

14.3.3. Les champs de bits

De tous les types de données qu'un programme peut avoir besoin de stocker, les booléens sont certainement l'un des plus importants. En effet, les programmes doivent souvent représenter des propriétés qui sont soit vraies, soit fausses. Après tout, la base du traitement de l'information telle qu'il est réalisé par les ordinateurs est le bit, ou chiffre binaire...

Il existe plusieurs manières de stocker des booléens dans un programme. La technique la plus simple est bien entendu d'utiliser le type C++ natif bool, qui ne peut prendre que les valeurs true et false. Les programmes plus vieux utilisaient généralement des entiers et des constantes prédéfinies ou encore une énumération. Malheureusement, toutes ces techniques souffrent du gros inconvénient que chaque information est stockée dans le type sous-jacent au type utilisé pour représenter les booléens et, dans la plupart des cas, ce type est un entier. Cela signifie que pour stocker un bit, il faut réserver un mot mémoire complet. Même en tenant compte du fait que la plupart des compilateurs C++ stockent les variables de type bool dans de simples octets, la déperdition reste dans un facteur 8. Bien entendu, cela n'est pas grave si l'on n'a que quelques bits à stocker, mais si le programme doit manipuler un grand nombre d'informations booléennes, cette technique est à proscrire.

Nous avons vu dans la Section 3.1.4 qu'il est possible de définir des champs de bits en attribuant un nombre de bits fixe à plusieurs identificateurs de type entier. Cette solution peut permettre d'économiser de la mémoire, mais reste malgré tout relativement limitée si un grand nombre de bits doit être manipulé. Afin de résoudre ce problème, la bibliothèque standard C++ fournit la classe template bitset qui, comme son nom l'indique, encapsule des champs de bits de tailles arbitraires. Le paramètre template est de type size_t et indique le nombre de bits que le champ de bits encapsulé contient.

Note : Vous noterez que cela impose de connaître à la compilation la taille du champ de bits. Cela est regrettable et limite sérieusement l'intérêt de cette classe. Si vous devez manipuler des champs de bits de taille dynamique, vous devrez écrire vous-même une classe d'encapsulation dynamique des champs de bits.

La classe bitset est déclarée comme suit dans l'en-tête bitset :

 
Sélectionnez

template <size_t N>
class bitset
{
public:
    class reference;    // Classe permettant de manipuler les bits.
 
    // Les constructeurs :
    bitset();
    bitset(unsigned long val);
    template<class charT, class traits, class Allocator>
    explicit bitset(
        const basic_string<charT, traits, Allocator> &chaine,
        typename basic_string<charT, traits, Allocator>::size_type debut = 0,
        typename basic_string<charT, traits, Allocator>::size_type taille =
            basic_string<charT, traits, Allocator>::npos);
 
    // Les fonctions de conversion :
    unsigned long to_ulong() const;
    template <class charT, class traits, class Allocator>
        basic_string<charT, traits, Allocator> to_string() const;
 
    // Les opérateurs de manipulation :
    bitset<N> &operator&=(const bitset<N> &);
    bitset<N> &operator|=(const bitset<N> &);
    bitset<N> &operator^=(const bitset<N> &);
    bitset<N> &operator<<=(size_t pos);
    bitset<N> &operator>>=(size_t pos);
    bitset<N> operator<<(size_t pos) const;
    bitset<N> operator>>(size_t pos) const;
    bitset<N>  operator~() const;
    bitset<N> &set();
    bitset<N> &set(size_t pos, bool val = true);
    bitset<N> &reset();
    bitset<N> &reset(size_t pos);
    bitset<N> &flip();
    bitset<N> &flip(size_t pos);
    bool test(size_t pos) const;
    reference operator[](size_t pos);   // for b[i];
 
    // Les opérateurs de comparaison :
    bool operator==(const bitset<N> &rhs) const;
    bool operator!=(const bitset<N> &rhs) const;
 
    // Les fonctions de test :
    size_t count() const;
    size_t size() const;
    bool any() const;
    bool none() const;
};
 

La construction d'un champ de bits nécessite de connaître le nombre de bits que ce champ doit contenir afin d'instancier la classe template bitset. Les différents constructeurs permettent d'initialiser le champ de bits en affectant la valeur nulle à tous ses bits ou en les initialisant en fonction des paramètres du constructeur. Le deuxième constructeur affectera aux premiers bits du champ de bits les bits correspondant de l'entier de type unsigned long fourni en paramètre, et initialisera les autres bits du champ de bits à la valeur 0 si celui-ci contient plus de bits qu'un unsigned long. Le troisième constructeur initialise le champ de bits à partir de sa représentation sous forme de chaîne de caractères ne contenant que des '0' ou des '1'. Cette représentation doit être stockée dans la basic_string fournie en premier paramètre, à partir de la position debut et sur une longueur de taille caractères. Cette taille peut être inférieure à la taille du champ de bits. Dans ce cas, le constructeur considérera que les bits de poids fort sont tous nuls et initialisera les premiers bits du champ avec les valeurs lues dans la chaîne. Notez bien que les premiers caractères de la chaîne de caractères représentent les bits de poids fort, cette chaîne est donc parcourue en sens inverse lors de l'initialisation. Ce constructeur est susceptible de lancer une exception out_of_range si le paramètre debut est supérieur à la taille de la chaîne ou une exception invalid_argument si l'un des caractères utilisés est différent des caractères '0' ou '1'.

Comme vous pouvez le constater d'après la déclaration, la classe bitset fournit également des méthodes permettant d'effectuer les conversions inverses de celles effectuées par les constructeurs. La méthode to_ulong renvoie donc un entier de type unsigned long correspondant à la valeur des premiers bits du champ de bits, et la méthode template to_string renvoie une chaîne de caractères contenant la représentation du champ de bits sous la forme d'une suite de caractères '0' et '1'. La classe bitset fournit également des surcharges des opérateurs operator<< et operator>> pour les flux d'entrée / sortie de la bibliothèque standard.

Exemple 14-25. Utilisation d'un bitset
Sélectionnez

#include <iostream>
#include <bitset>
#include <string>
 
using namespace std;
 
int main(void)
{
    // Construit un champ de bits :
    string s("100110101");
    bitset<32> bs(s);
    // Affiche la valeur en hexadécimal de l'entier associé :
    cout << hex << showbase << bs.to_ulong() << endl;
    // Affiche la valeur sous forme de chaîne de caractères :
    string t;
    t = bs.to_string<string::value_type, string::traits_type,
        string::allocator_type>();
    cout << t << endl;
    // Utilise directement << sur le flux de sortie :
    cout << bs << endl;
    return 0;
}
 

Note : La méthode to_string est une fonction template ne prenant pas de paramètres. Le compilateur ne peut donc pas réaliser une instanciation implicite lors de son appel. Par conséquent, vous devrez fournir la liste des paramètres template explicitement si vous désirez utiliser cette méthode. Il est généralement plus simple d'écrire la valeur du bitset dans un flux standard.

Les modificateurs de format de flux hex et showbase ont pour but d'effectuer l'affichage des entiers sous forme hexadécimale. La personnalisation des flux d'entrée / sortie sera décrite en détail dans le Chapitre 15.

Les opérateurs de manipulation des champs de bits ne posent pas de problème particulier puisqu'ils ont la même sémantique que les opérateurs standards du langage, à ceci près qu'ils travaillent sur l'ensemble des bits du champ en même temps. Le seul opérateur qui demande quelques explications est l'opérateur d'accès unitaire aux bits du champ, à savoir l'opérateur operator[]. En effet, cet opérateur ne peut pas retourner une référence sur le bit désigné par son argument puisqu'il n'y a pas de type pour représenter les bits en C++. Par conséquent, la valeur retournée est en réalité une instance de la sous-classe reference de la classe bitset. Cette sous-classe encapsule l'accès individuel aux bits d'un champ de bits et permet de les utiliser exactement comme un booléen. En particulier, il est possible de faire des tests directement sur cette valeur ainsi que de lui affectuer une valeur booléenne. Enfin, la sous-classe reference dispose d'une méthode flip dont le rôle est d'inverser la valeur du bit auquel l'objet reference donne accès.

La classe template bitset dispose également de méthodes spécifiques permettant de manipuler les bits sans avoir recours à l'opérateur operator[]. Il s'agit des méthodes test, set, reset et flip. La première méthode permet de récupérer la valeur courante d'un des bits du champ de bits. Elle prend en paramètre le numéro de ce bit et renvoie un booléen valant true si le bit est à 1 et false sinon. La méthode set permet de réinitialiser le champ de bits complet en positionnant tous ses bits à 1 ou de fixer manuellement la valeur d'un bit particulier. La troisième méthode permet de réinitialiser le champ de bits en annulant tous ses bits ou d'annuler un bit spécifique. Enfin, la méthode flip permet d'inverser la valeur de tous les bits du champ ou d'inverser la valeur d'un bit spécifique. Les surcharges des méthodes qui travaillent sur un seul bit prennent toutes en premier paramètre la position du bit dans le champ de bits.

Exemple 14-26. Manipulation des bits d'un champ de bits
Sélectionnez

#include <iostream>
#include <string>
#include <bitset>
 
using namespace std;
 
int main(void)
{
    // Construit un champ de bits :
    string s("10011010");
    bitset<8> bs(s);
    cout << bs << endl;
    // Inverse le champ de bits :
    bs.flip();
    cout << bs << endl;
    // Fixe le bit de poids fort :
    bs.set(7, true);
    cout << bs << endl;
    // Annule le 7ème bit à l'aide d'une référence de bit :
    bs[6] = false;
    cout << bs << endl;
    // Anule le bit de poids faibe :
    bs.reset(0);
    cout << bs << endl;
    return 0;
}
 

Enfin, la classe bitset fournit quelques méthodes permettant d'effectuer des tests sur les champs de bits. Outre les opérateurs de comparaison classiques, elle fournit les méthodes count, size, any et none. La méthode count renvoie le nombre de bits positionnés à 1 dans le champ de bits. La méthode size renvoie quant à elle la taille du champ de bits, c'est-à-dire la valeur du paramètre template utilisée pour instancier la classe bitset. Enfin, les méthodes any et none renvoient true si un bit au moins du champ de bits est positionné ou s'ils sont tous nuls.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 developpez.com Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.