Mise à jour du 22/05/2024.
Nous l'avons vu dans l'introduction à la réalisation d'un serveur d'application, le type ou la classe des données à stocker en session ou dans le contexte n'est pas connu à l'avance. En Java, ce n'était pas un problème car ce langage d'une part repose sur le fait que tout objet hérite de la classe Object et d'autre part que toute les méthodes des objest sont virtuelles. Il suffit donc de stocker les objets en les manipulant par une référence sur leur classe ancêtre pour pouvoir stocker des objets de classe quelconque. Lors de leur lecture, l'instrospection et le polymorphiqme permettent de retrouver la dernière classe dérivée et de manipuler l'objet via une référence sur cette dernière. Malheureusement en C++, il n'y a pas d'ancêtre commun, il n'y a pas d'obligation d'héritage simple et les méthodes ne sont pas obligatoirement vituelles. Pire, les types scalaires (int, char, long, double, etc.) ne disposent pas d'une classe associée (mécanisme dit du boxing).' Or nous voulons pouvoir stocker aussi bien des objets (instances de classe) que des données de type scalaire.
La solution la plus simple est d'imposer que chaque donnée stockée hérite d'une classe ancêtre commune. C'est une référence sur cette classe ancêtre qui est ensuite stockée.
![]() | Comme on le voit sur le schéma ci-contre, le dictionnaire de l'application ou de la session ne stocke que des "références" sur des objets de classe IData. Toutefois, ce sont bien deux objets respectivement de classe DataString et DataLong qui sont référencés via leur classe ancêtre. |
Le terme "référence" fait ici allusion à des "pointeurs intelligents". Pourquoi des pointeurs intellignets et non des variables, des références ou des pointeurs ? En fait, en C++ les choses ne sont pas si simples. Tout va bien si l'objet est créer sur la pile car il y demeurera jusqu'à sa suppression explicite (instruction delete) ou l'arrêt de l'application (et donc la purge de son espace d'adressage par l'OS). De tels objets peuvent donc être manipulés par des pointeurs ou des références (le &qui suit un identifiant de type ou de classe). En revanche, si l'objet est créé sur le tas il est supprimé automatiquement dès qu'il sort de la portée du code qui l'a créé. Le pointeur ou la référence dmeurent mais ils désignent alors une zone de mémoire qui n'est plus allouée (bonjour les dégâts !). Ce type de bug est suffisamment subtil pour donner des cauchemards à tout développeur. Le pointeur intellignet impose la création de l'objet sur la pile ce qui évite ce type de désagrément.
Voici le code source de base de notre classe ancêtre IData :
/**
* @brief La classe AnyData est une classe abstraite que doit hériter toute classe qui a besoin d'être stockée
* dans des données de contexte ou de session.
* Les méthodes virtuelles pures <code>isNull</code> et <code>toString</code> devront être implémentéee par la classe
* concrète dérivée.
*/
class IData {
public:
explicit IData(const QString& name);
virtual ~IData();
const QString name();
virtual IData* value() { return this; }
/**
* @brief isNull Lecture de l'état de l'indicateur d'état indéfini pour cet objet.
* @return Etat de l'indicateur d'état indéfini pour cet objet.
* @throws Exception : méthode non surchargée.
*/
virtual bool isNull() {
QString err = "Méthode non encore implémentée";
throw Exception(err);
};
/**
* @brief toString Transformation de l'état de l'objet en une chaîne de caractères affichable.
* @return Etat de l'objet sous forme d'une chaîne de caractères affichable.
* @throws Exception : méthode non surchargée.
*/
virtual QString toString() {
QString err = "Méthode non encore implémentée";
throw Exception(err);
};
protected:
/**
* @brief _name Nom de la classe.
*/
QString _name;
};
La méthode name() est une facilité qui permet l'identifiaction d'une classe par son nom.
A titre d'exemple, nous allons montrer comment stocker une chaîne de caractères et un entier long dans une même liste. Ces objets seront ensuite modifiés depuis cette liste et relus pour en vérifier la modification.
#include <QCoreApplication> #include <iostream> #include "idata.hpp" using namespace std; class DataString : public IData { public: DataString(const QString& value) : IData{"DataString"} { _value = value; } DataString(DataString& data) : IData{"DataString"} { _value = data._value; } virtual bool isNull() { return false; } virtual QString toString() { return _value; } virtual QString& content() { return _value; } virtual QString setValue(const QString value) { QString old = _value; _value = value; return old; } DataString& operator =(const QString& value) { _value = value; return *this; }; DataString& operator +=(const QString& value) { _value += value; return *this; }; protected: QString _value; }; class DataLong : public IData { public: DataLong(long value) : IData{"DataLong"} { _value = value; } DataLong(DataLong& data) : IData{"DataLong"} { _value = data._value; } virtual bool isNull() { return _value == 0; } virtual QString toString() { return QString::number(_value); } virtual long content() { return _value; } virtual long setValue(long value) { long old = _value; _value = value; return old; } DataLong& operator =(long value) { _value = value; return *this; }; DataLong& operator *=(long value) { _value *= value; return *this; }; protected: long _value; }; QList<shared_ptr<IData>> initData() { cout << "Entrée dans la fonction initData..." << endl; QList<shared_ptr<IData>> list; shared_ptr<DataString> spData1 = make_shared<DataString>("Test 1"); list.append(spData1); shared_ptr<DataLong> spData2 = make_shared<DataLong>(123456789L); list.append(spData2); return list; } void printData(IData& data) { cout << data.toString().toStdString() << endl; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QList<shared_ptr<IData>> list = initData(); // Affichage de l'état des objets de la liste for (auto pI = list.begin(); pI != list.end(); ++pI) { shared_ptr<IData> spData = *pI; IData* pData = spData->value(); printData(*pData); } // Modification des objets de la liste for (auto pI = list.begin(); pI != list.end(); ++pI) { shared_ptr<IData> spData = *pI; IData* pData = spData->value(); if (pData->name() == "DataString") { DataString* p = reinterpret_cast<DataString*>(pData); *p += " (suffixe ajouté)"; } else if (pData->name() == "DataLong") { DataLong* p = reinterpret_cast<DataLong*>(pData); *p *= 10; } else throw Exception(QString("Type de la donnée stockée inconnu : [%1]").arg(pData->name())); } // Affichage de l'état des objets modifiés de la liste for (auto pI = list.begin(); pI != list.end(); ++pI) { shared_ptr<IData> spData = *pI; IData* pData = spData->value(); DataString* p = reinterpret_cast<DataString*>(pData); printData(*p); } //return app.exec(); return 0; }
La sortie console du programme ressemble à ceci :
Entrée dans la fonction initData... Test 1 123456789 Test 1 (suffixe ajouté) 1234567890
Dans cet exemple, nous utilisons la méthode name() pour transtyper les pointeurs sur des objets instanciés à partir des classes dérivées de la classe ancêtre IData.
Outre leur intérêt d'assurer la création des objets à stocker sur la pile, les pointeurs intelligents ont également l'immense avantage de ne pas avoir à se préoccuper de leur désallocation. Leur emploi limite fortement le risque de "fuite mémoire".
Rédaction par Jean-Marie Piatte (1983-2021)