Classe CDumpable ou CEditable ?

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

Sommaire

Introduction
Transformation d'un objet en chaîne de caractères
Surcharge de l'injecteur <<
Classe CEditable
Edition de classes complexes
Classe CDumpable
Manipulateurs
Bibliographie

Introduction

    Il est très fréquent que le contenu d'un objet d'une classe doive être mis sous une forme textuelle. On peut en trouver plusieurs bonnes raisons, pour lesquelles des solutions générales ou spécifiques au C++ seront développées plus loin : Sommaire

Transformation d'un objet en chaîne de caractères

    Le schéma standard fourni par Java peut être adopté en C++ : en Java, toute classe dérive obligatoirement de la classe objects, dont la fonction membre (la méthode) toString() renvoie une chaîne de caractères qui est la représentation textuelle de l'objet. Toutes les fonctions étant virtuelles en Java, et le polymorphisme systématique, il suffit de surcharger cette fonction pour n'importe quelle sous-classe d'une hiérarchie pour que ce soit cette dernière fonction qui soit utilisée [PRAD99] .

    En C++, le même mécanisme n'est pas automatique mais peut être aisément mis en place, en créant une classe générale virtuelle CObject, abstraite ou pas, qui pourrait être écrite et utilisée ainsi :
 
class CObject
{
    ...
  public :
    virtual string toString (void) const { return string (); }

}; // CObject
...
class CRationnel : public CObject

  protected :
    int m_Num; 
    int m_Denom; 
    ...
  public :
    CRationnel (const int Num = 0, const int Denom = 1);

    virtual string toString (void) const;
    ...
}; // CRationnel

 
string CRationnel::toString (void) const
{
    const unsigned CstSize = 128;

    char Tab [CstSize];
    ostrstream ostr (Tab, CstSize);

    ostr << m_Num << '/' << m_Denom << ends;

    return Tab;

}; // toString()

 
void Editer (const CObject & Obj)
{
    cout << Obj.toString();     // Utilisation du polymorphisme

} // Editer()

 
Editer (CRationnel ( 1,  3));
cout << endl; 

CRationnel R1 (12, 23);
string Texte ("Rationnel = ");
Texte += R1.toString() + "; ";  // Utilisation dans une autre chaîne
cout << Texte << endl;

    Le principal inconvénient de cette méthode est la création d'une chaîne intermédiaire, quelle qu'en soit la destination ultérieure, même si elle est immédiatement éditée dans un flux et plus jamais utilisée. Une première solution est de rendre la classe CRationnel "éditable", soit en surchargeant l'opérateur injecteur <<, soit en la faisant dériver d'une classe virtuelle CEditable. Une autre solution élégante offerte par le C++, et très rarement utilisée, est la construction d'un manipulateur.

Sommaire


Surcharge de l'injecteur <<

    Rappelons le principe de la surcharge de l'injecteur :
 
class CRationnel

  protected :
    int m_Num; 
    int m_Denom; 
    ...
  public :
    CRationnel (const int Num = 0, const int Denom = 1);

    friend ostream & operator << (ostream & os, const CRationnel & R);
    ...
}; // CRationnel

 
ostream & operator << (ostream & os, const CRationnel & R)
{
    return os << R.m_Num << '/' << R.m_Denom;

} // operator <<()

 
cout << CRationnel (123, 45) << endl;
cerr << CRationnel ( 3,  1)  << endl;

    Le principal inconvénient de cette méthode est de ne pas permettre le polymorphisme. Chaque classe d'une hiérarchie doit avoir son propre opérateur << surchargé, et c'est celui de la classe indiquée qui est appelé, et non celui de la classe réelle, l'opérateur << n'étant pas une fonction virtuelle de la classe. Considérons l'exemple suivant d'une classe CRationnelInf1, pour laquelle le numérateur est toujours inférieur ou égal au dénominateur, et dont l'injecteur est surchargé pour afficher la valeur réelle du rationnel :
 
class CRationnelInf1 : public CRationnel

    ...
  public :
    ...
    friend ostream & operator << (ostream & os, const CRationnel & R);
    ...
}; // CRationnelInf1
 
ostream & operator << (ostream & os, const CRationnelInf1 & R)
{
    return os << (static_cast <float> (R.m_Num) / static_cast <float> (R.m_Denom));

} // operator << ()

 
void EditerRationnel (const CRationnel & Obj)
{
    cout << Obj << endl;

} // EditerRationnel()

    Comme le montre la séquence suivante, l'affichage obtenu par la fonction EditerRationnel() n'a pas la présentation voulue, le polymorphisme n'étant pas disponible :
 
                                            // Affichage de :
cout << CRationnelInf1 (12, 24) << endl;    //   0.5
cerr << CRationnelInf1 (12, 36) << endl;    //   0.333333
EditerRationnel (CRationnel     (12, 24));  //   12/24
EditerRationnel (CRationnelInf1 (12, 36));  //   12/36 au lieu de 0.333333

    Rien n'empêche d'utiliser la surcharge de << pour transformer un rationnel en chaîne de caractères, grâce aux "flux-chaînes" du C++, mais l'écriture est un peu plus lourde et surtout, doit être recommencée à chaque utilisation :
 
const unsigned CstSize = 128;

char Tab [CstSize];
ostrstream ostr (Tab, CstSize);
ostr << CRationnelInf1 (12, 48) << ends;

cout << Tab << endl;

Remarque : la surcharge de << n'interdit en rien la transformation en chaîne de caractères, en faisant aussi dériver la classe CRationnel de CObject.

Sommaire


Classe CEditable

    Pour pallier cet inconvénient, il est possible de faire dériver toute classe qui doit être affichée dans un flux d'une classe virtuelle CEditable, abstraite ou non, qui est la seule à surcharger l'opérateur <<, selon le schéma suivant :
 
class CEditable
{
  protected :
    virtual ostream & _Edit [1] (ostream & os) const = 0;

  public :
    friend ostream & operator << (ostream & os, const CEditable & Obj);

}; // CEditable

 
inline ostream & operator << (ostream & os, const CEditable & Obj)
{
    return Obj._Edit (os);

} // operator <<

 
class CRationnel : public CEditable

  protected :
    int m_Num; 
    int m_Denom; 

    virtual ostream & _Edit (ostream & os) const
    { 
        return os << m_Num << '/' << m_Denom;

    } // _Edit();
    ...
  public :
    CRationnel (const int Num = 0, const int Denom = 1);

   ...
}; // CRationnel

 
class CRationnelInf1 : public CRationnel

    ...
  protected :
    virtual ostream & _Edit (ostream & os) const
    {
        return os << (static_cast <float> (R.m_Num) / 
                      static_cast <float> (R.m_Denom));
    } // _Edit()
    ...
}; // CRationnelInf1
 
void EditerRationnel (const CRationnel & Obj)
{
    cout << Obj << endl;

} // EditerRationnel()

    Cette fois, la même séquence que précédemment montre que le polymorphisme est mis en oeuvre :
 
                                            // Affichage de :
cout << CRationnelInf1 (12, 24) << endl;    //   0.5
cerr << CRationnelInf1 (12, 36) << endl;    //   0.333333
EditerRationnel (CRationnel     (12, 24));  //   12/24
EditerRationnel (CRationnelInf1 (12, 36));  //   0.333333

Sommaire


Edition de classes complexes

    Cette solution, bien que très séduisante, présente cependant quelques difficultés : Remarque : comme précédemment, l'utilisation d'une classe CEditable n'interdit en rien la transformation en chaîne de caractères, C++ offrant la possibilité de dérivation multiple :
 
class CRationnel : public CObject, public CEditable { ... };

Sommaire


Classe CDumpable

    La notion de classe "dumpable" [MSVC5] est étroitement liée à celle de débogage : dumper un objet consiste à afficher les valeurs de toutes ses données membres, par exemple afin de vérifier si elles sont correctement initialisées. Malgré une évidente analogie avec le caractère "éditable" d'une classe, il faut noter cependant plusieurs différences :     L'implémentation d'une classe CDumpable peut être très analogue à celle de CEditable. Cependant, la surcharge de l'opérateur << pour les deux types de classes peut provoquer des ambiguïtés à la compilation. Comment les deux instructions suivantes pourraient-elles être différenciées :
 
CEmploye Toto (...);

cout << Toto << endl;
cerr << Toto << endl;

    Une solution serait de différencier les flux, en créant une classe de flux CFluxDump, dérivée de ostream. Toute injection dans ce type de flux serait un "dump", toute autre injection serait une "édition" : voir [DEV99].

Sommaire

Manipulateurs

    Pour formatter une édition dans un flux, par exemple, il n'est pas toujours nécessaire de surcharger l'opérateur <<, ou de rendre une classe ou un type "éditable". Il suffit de définir un manipulateur. L'exemple ci-dessous permet d'afficher une date de la classe CDate dans le format jj-mm-aa en utilisant le manipulateur générique COmanip présenté dans le chapitre correspondant :
 
class CDate
{
    int m_Jour;
    int m_Mois;
    int m_Annee;
  public :
    CDate (int Jour, int Mois, int Annee)
        : m_Jour (Jour), m_Mois (Mois), m_Annee (Annee) {}
    friend ostream & EditDate (ostream & os, const CDate & Date)
    {
        return os << Date.m_Jour << '-'
                     << Date.m_Mois << '-'
                     << Date.m_Annee;

    } // EditDate()

}; // CDate

COmanip Edit (const CDate & Date)
{
    return COmanip (EditDate, Date);

} // Edit()

void Essai (void)
{
    CDate D (12, 4, 2001);
    cout << Edit (D) << endl;

} // Essai()

Sommaire


Bibliographie

[PPRAD99] http://perso.club-internet.fr/pprados/Langage/Java/toString/toString.html
[MSVC5] voir la classe CDump de Visual C++ Microsoft
[DEV99] voir devoir système 1999-2000 - IUT Aix - Dépt Info 

Sommaire


[1] Nous avons fait précéder l'identificateur de la fonction _Edit() d'un caractère souligné, car il n'est pas improbable qu'un utilisateur, faisant dériver une de ses propres classe de CEditable, ait déjà une fonction Edit(), ce qui pourrait provoquer un conflit. Il est recommandé de ne pas utiliser les identificateurs précédés d'un ou deux caractères _ comme identificateurs "normaux".

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