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 :

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

#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

#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 :

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

#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.