Manipulateurs

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 19/01/2000 - Dernière mise à jour : 26/02/2001

Manipulateurs standard

    De nombreux manipulateurs standard sont mis à la disposition des utilisateurs des flux pour faciliter le formattage des E/S.
 
ostream & endl   (ostream &); // insert newline and flush
ostream & ends   (ostream &); // insert null to terminate string
ostream & flush  (ostream &); // flush the ostream
ios     & dec    (ios &);     // set conversion base to decimal
ios     & hex    (ios &);     // set conversion base to hexadécimal
ios     & oct    (ios &);     // set conversion base to octal

On peut donc écrire :
 
cout << hex << 12 << endl;

Manipulateurs de l'utilisateur

Manipulateurs non paramétrés

    Les manipulateurs standard agissent sur les caractéristiques du flux, en modifiant ses données membres. Lorsqu'un affichage particulier apparaît fréquemment dans un programme, il est élégant de l'effectuer au moyen d'un manipulateur.

    Dans le document "Fonctions de gestion des dates et du temps" est présentée une fonction Estampiller() qui affiche la date courante dans le flux qui lui est passé en paramètre. Elle est difficile à utiliser du fait qu'elle ne peut être insérée dans une suite d'injections. Elle devient beaucoup plus élégante si elle est transformée en un manipulateur : il suffit pour cela de lui faire renvoyer le flux qui lui est passé en paramètre :
 
ostream & Estampiller (ostream & os)
{
    std::time_t t = std::time (0);
    return os << "date : " << std::asctime (std::localtime (&t));

} // Estampiller()

et de l'utiliser de la façon suivante :
 
#include <iomanip>

cerr << "####" << Estampiller << "----" << endl; //         ligne {1}
cout << "####" << Estampiller << "----" << endl;

qui provoque l'affichage suivant :
 
####date : Mon Feb 12 21:20:40 2001
----
[1]
####date : Mon Feb 12 21:20:40 2001
----

Un manipulateur non paramétré est une fonction qui a pour unique paramètre un flux ostream passé par référence et qui renvoie ce flux."

    Il faut tout d'abord remarquer que, dans la ligne [1] ci-dessus, il ne s'agit pas du tout de l'appel de la fonction Estampiller (qui aurait été écrit Estampiller(cerr)). Rappelons aussi que l'identificateur d'une fonction, sans les (), est un pointeur vers cette fonction (c'est-à-dire qu'il représente l'adresse à laquelle est le code de la fonction).

    Le second opérande Estampiller (opérande droit) du second opérateur << de la ligne [1] est donc un pointeur vers une fonction qui a pour paramètre un flux ostream passé par référence et qui renvoie aussi la référence d'un flux ostream. En C++, le type de cet opérante s'écrit :
 
ostream & (*) (ostream &)

    Il faut donc qu'il existe quelque part une surcharge de l'opérateur << ayant pour profil :
 
ostream & operator << (ostream &, ostream & (*) (ostream &));

    Cette surcharge existe dans le fichier <iomanip>, dont la définition pourrait être :
 
ostream & operator << (ostream & os, ostream & (*f) (ostream &))
{
    return f (os);
}

Manipulateurs paramétrés

Manipulateur à un paramètre booléen : CImage

    Si la fonction nécessite un paramètre supplémentaire, le problème devient beaucoup plus complexe. Considérons par exemple le manipulateur Image qui affiche les chaînes true ou false selon la valeur d'un booléen (l'injecteur << affiche 1 ou 0) qui lui est passé en paramètre. Par analogie avec le manipulateur Estampiller(), on pourrait l'écrire ;
 
ostream & Image (ostream & os, bool Val)
{
    return os << (Val ? "true" : "false");

} // Image()

    Il serait aussi nécessaire de surcharger de nouveau l'opérateur << ainsi :
 
ostream & operator << (ostream & os, ostream & (*) (ostream &, bool));

    Il serait de toutes façons impossible d'utiliser cette fonction en lui passant un paramètre booléen sans que ce soit un appel, aucune des deux écritures ci-dessous n'étant valide :
 
cout << Image << (1 == 3) << endl; // le booléen n'est pas passé en paramètre
cout << Image    (1 == 3) << endl; // la fonction est évaluée, et son résultat est injecté dans le flux

    La solution consiste à créer une classe CImage, telle que CImage (3) représente le constructeur d'un objet, et que l'objet ainsi construit puisse être injecté dans un flux. Cela revient à dire soit que la classe CImage est CEditable, soit que l'opérateur injecteur << est surchargé pour cette classe. On peut écrire par exemple :
 
class CImage
{
    bool m_Bool;
  public :
    CImage (bool Bool) : m_Bool (Bool) {}

    friend ostream & operator << (ostream &, const CImage & Val);

}; // CImage

ostream & operator << (ostream & os, const CImage & Val)
{
    return os << (Val.m_Bool ? "true" : "false");

} // operator <<

et :
 
cout << "1 == 3 : " << CImage (1 == 3) << endl; // 1 == 3 : false
cout << "2 == 2 : " << CImage (2 == 2) << endl; // 2 == 2 : true

    Cette technique, qui consiste à faire effectuer l'affichage par un objet d'une classe pour laquelle l'opérateur << est surchargé, est attribuée par B. Eckel à J. Schwartz. Un tel manipulateur est appelé un effector.

Manipulateur à un paramètre entier : CBin

    L'exemple ci-dessous montre un manipulateur qui affiche en binaire un entier non signé :
 
class CBin
{
    unsigned m_Unsigned;
  public :
    CBin (unsigned Unsigned) : m_Unsigned (Unsigned) {}

    friend ostream & operator << (ostream &, const CBin & Val);

}; // CBin

ostream & operator << (ostream & os, const CBin & Val)
{
    for (unsigned Mask = 1 << (8 * sizeof (unsigned) - 1); Mask; )
    {
        os << (Mask & Val.m_Unsigned ? '1' : '0');
        Mask >>= 1;
    }
    return os;

} // operator <<

et qui peut être utilisé ainsi :
 
cout << "0xFFFFFFFF = " << CBin (0xFFFFFFFF) << '\n'
     << "0xF0F0F0F0 = " << CBin (0xF0F0F0F0) << '\n'
     << "0xOOOOOOOF = " << CBin (0x0000000F) << '\n'
     << "0xOOOOOOO0 = " << CBin (0x00000000) << endl;

Manipulateur à un paramètre générique : CEntreAccolades

    Le manipulateur présenté ici permet d'afficher entre {} la valeur d'un objet quelconque, pourvu que cette valeur soit affichable au moyen de l'injecteur. Il a la même structure que CImage ci-dessus, mais le type bool est remplacé par un type générique :
 
template <typename T> class CEntreAccolades
{
    T m_Info;
  public :
    CEntreAccolades (const T & Info) : m_Info (Info) {}

    friend ostream & operator << (ostream & os, const CEntreAccolades & Val)
    {
        return os << '{' << Val.m_Info << '}';
    }

}; // CEntreAccolades

qui peut être utilisé ainsi :
 
cout << CEntreAccolades <int> (3) << "; "
     << CEntreAccolades <int, '<', '>'> (-12) << endl;

Fonction génératrice de manipulateur à un paramètre générique

    A l'évidence, la technique exposée ci-dessus n'est pas très commode à utiliser, chaque manipulateur devant être instancié avant d'être utilisé. Rappelons que, contrairement aux classes génériques qui doivent être explicitement instanciées avant leur utilisation, les fonctions génériques sont automatiquement instanciées par le compilateur lors de leur utilisation Il convient alors d'utiliser une très élégante solution qui passe par l'écriture d'une fonction qui construit elle-même l'objet nécessaitre :
 
template <typename T> CEntreAccolades <T> makeEntreAccolades (const T & Info)
{
     return CEntreAccolades <T> (Info);

} // makeEntreAccolades()

et :
 
cout << makeEntreAccolades (1234) :   << endl;
     << makeEntreAccolades ("azerty") << endl;

Classe générique de manipulateurs

    Les techniques présentées ci-dessus nécessitent que le développeur du manipulateur soit capable d'écrire lui-même la fonction d'édition, lors de la surcharge de l'injecteur. Il est des situations où la fonction d'édition existe mais seul son profil est connu, son implémentation étant livrée sous forme compilée. Supposons par exemple que nous ne connaissions que la signature de la fonction uint2bin() qui affiche un entier non signé en binaire :
 
ostream & uint2bin (ostream & os, const unsigned & Val);

    Non seulement le manipulateur doit stocker la valeur à traiter, mais aussi la fonction à effectuer lors de l'appel de l'injecteur. Il doit donc avoir la forme suivante :
 
class Cuint2bin
{
   private :
     ostream & (*m_Traitement) (ostream &, const unsigned &);
     unsigned m_Param;

   public :
     Cuint2bin (ostream & (*Traitement) (ostream &, const unsigned &), unsigned Param)
         : m_Traitement (Traitement), m_Param (Param) {}
     friend ostream & operator << (ostream & os, const Cuint2bin & m)
     {
         return m.m_Traitement (os, m.m_Param);
     }

}; // Cuint2bin

    Il suffit maintenant d'écrire une fonction qui crée un manipulateur de ce type :
 
Cuint2bin Bin (unsigned uint) { return Cuint2bin (uint2bin, uint); }

et de l'utiliser le plus simplement :
 
cout << "0xFFFFFFFF = " << Bin (0xFFFFFFFF) << '\n'
     << "0xF0F0F0F0 = " << Bin (0xF0F0F0F0) << '\n'
     << "0xOOOOOOOF = " << Bin (0x0000000F) << '\n'
     << "0xOOOOOOO0 = " << Bin (0x00000000) << endl;

    La fonction uint2bin() pourrait ùtre écrite :
 
ostream & uint2bin (ostream & os, const unsigned & Val)
{
     for (unsigned Mask = 1 << (8 * sizeof (unsigned) - 1); Mask; )
     {
        os << (Mask & Val ? '1' : '0');
        Mask >>= 1;
     }
     return os;

} // uint2bin()

Manipulateur "universel"

    Le manipulateur ignore tout de la fonction qu'il appliquera lors de l'injection. Il peut aussi tout ignorer du paramètre qu'il traite : il suffit de le rendre générique. Stroustrup définit ainsi un manipulateur générique :
 
template <class T> class COmanip
{
   private :
     ostream & (*m_Traitement) (ostream &, const T &);
     T m_Param;

   public :
     COmanip (ostream & (*Traitement) (ostream &, const T &), T Param)
         : m_Traitement (Traitement), m_Param (Param) {}

     friend ostream & operator << (ostream & os, const COmanip & m)
     {
         return m.m_Traitement (os, m.m_Param);
     }

}; // COmanip

    L'utilisation d'un tel manipulateur pour afficher des valeurs entre crochets, nécessite de plus :
 
template <class T>
ostream & Crochets (ostream & os, const T & i)
{
     return os << '[' << i << ']';

} // Crochets()

template <typename T>
COmanip <T> ManipCrochets (const T & i)
{
     return COmanip <T> (Crochets, i);

} // ManipCrochets()

et :
 
cout << ManipCrochets (12)    << endl;
cout << ManipCrochets (12.34) << endl;

Classe smanip

    Une classe analogue à la classe COManip, la classe smanip, existe dans la norme. Celle-ci n'impose pas son implémentation. Elle est accessible par le fichier <iomanip>. Le compilateur propose plusieurs autres manipulateurs génériques dans le fichier /usr/include/g++-2/iomanip.h>.
 
template <class T> class imanip;
template <c1ass T> class omanip;
template <class T> class iapp;
template <class T> class oapp;

    Plusieurs des manipulateurs standard sont implémentés au moyen de ces classes génériques.


[1] Rappelons que la chaîne renvoyée par la fonction asctime() est terminé:e par le caractère '\n'.

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique