Gérer ses données en C++


précédentsommairesuivant

III. L'équipement

Un bon artisan n'étant pas grand chose sans ses outils, définissons ceux dont nous devons nous munir.

III-A. La gestion des erreurs

L'erreur est humaine... mais pas que. Durant l'exécution du programme utilisateur, plusieurs disfonctionnements peuvent survenir comme l'échec d'ouverture d'un fichier, un problème de mémoire, réseau etc. Aussi, il se peut que nous ou notre utilisateur fasse une fausse manipulation, des erreurs d'inattention, de déconcentration, une mauvaise configuration ou autre, ce qui est tout à fait légitime. Pour prévenir ces soucis, il va nous falloir les gérer. Non pas les régler à la place du "fautif", mais l'amener facilement et efficacement à l'origine du problème afin qu'il puisse le régler dans un délai réduit, sans trop se casser la tête (si c'est un bon informaticien, il n'aimera pas ça).

Nous allons donc fournir une espèce de "stack trace" des erreurs, c'est à dire reproduire une pile d'erreurs comme "Le traitement a échoué" car "le module xxx n'a pu être initialisé" car "le fichier file.ini est inaccessible".

III-A-1. Les exceptions

Il y a plusieurs manières de faire pour notifier un disfonctionnement :

  • L'une procédurale, consiste à renvoyer un booléen ou un code erreur à l'appelant pour dire "j'ai fait le boulot mais par contre j'ai eu des soucis"
  • L'autre, plus dans un contexte de responsabilité (objet), consiste à tout arrêter et à notifier le supérieur qu'on ne peut plus avancer, via les exceptions, des objets encapsuleurs de problèmes.

C'est cette deuxième méthode que nous allons mettre en place, pour plusieurs raisons :

  • Cela implique de ne peut pas passer un problème sous le manteau, créant d'autres problèmes de plus en plus sournois, nécessitant d'avantage de temps de correction.
  • Les informations sur le disfonctionnement seront plus précises.
  • Le code sera plus lisible, les valeurs de retour ne seront plus monopolisées, pas besoin de faire des assert.
  • Une exception étant un objet, il peut encapsuler une éventuelle exception d'origine qui nous garantira une certaine traçabilité de problème, notre "stack trace".

Une bonne pratique est de faire un try/catch autour de la fonction main, pour qu'en cas de non traitement d'une exception, obtenir au moins un rapport explicite allant du plus haut niveau de l'erreur vers son plus bas niveau. Ce genre de rapport pourra être "copié/collé" directement dans un outils de suivi d'anomalie, ou automatiquement par mail si nous décidons d'implémenter une fonctionnalité d'envoi de disfonctionnement etc.

III-A-2. Conception

Une solution ressemble un peu au pattern composite, pour gérer la traçabilité. Définissons ce que possède une exception :

  • Un message
  • Une éventuelle exception à l'origine
  • D'éventuelles causes
  • D'éventuelles pistes de résolution
  • D'éventuelles paramètres périphériques

Les exceptions spécifiques alimenteront les données et non le lanceur de celles-ci pour une simple raison, que les sous classes imposent un minimum de paramètres afin d'être assez explicite.

 
Sélectionnez
// Ce que nous ne proposerons pas :
CFileAccessException except;
except.SetMessage("Impossible d'accéder au fichier");
except.AddParam("FICHIER", strFile);
except.AddCause("Vous n'avez pas les droits d'accéder au fichier");
except.AddCause("Le fichier est verrouillé");
throw except;

// Et éviter ceci :
CFileAccessException except;
throw except; // Qui n'aurai aucune valeur informative si ce n'est que ça a planter

// Et mettons en place ceci :
throw CFileAccessException(strFile);

// En spécifiant les détails dans l'exception elle même
CFileAccessException::CFileAccessException(const std::string& rstrFile)
{
	SetMessage("Impossible d'accéder au fichier");
	
	AddParam(rstrFile);
	
	AddCause("Vous n'avez pas les droits d'accéder au fichier");
	AddCause("Le fichier est verrouillé");
}


Aussi, cette classe de base dérivera de std::exception et ne sera pas abstraite pour pouvoir lancer une exception dite "libre".

Image non disponible


Au fur et à mesure que nous aurons besoin de lancer de nouvelles exception, nous feront hériter d'avantage de classe de la même manière.

Image non disponible

Voyons le code de l'exception de base :

 
Sélectionnez
/*!
 * \brief Classe de base de nos exceptions.
 *
 * Toute exception peut contenir une exception d'origine.
 */
class CException
{
public :
	/*!
	 * \brief Constructeur par défaut.
	 *
	 * \param [in] poOriginException L'éventuelle exception à l'origine de celle-ci.
	 * \param [in] rstrMessage Le message associé à cette exception.
	 */
	CException(const CException* poOriginException = 0);

	/*!
	 * \brief Destructeur.
	 */
	virtual ~CException();

	/*!
	 * \brief Obtient l'exception à l'origine de celle-ci.
	 *
	 * \return L'exception à l'origine de celle-ci.
	 */
	const CException* GetOriginException() const;

	/*!
	 * \brief Obtient le message descriptif de cette exception.
	 *
	 * \return Le message descriptif de cette exception.
	 */
	virtual const char* what() const;

	/*!
	 * \brief Obtient la description complète de cette exception.
	 *
	 * \param [in] rstrLinePrefix Le préfixe à appliquer à chaque ligne de cette description.
	 * \return La description complète de cette exception.
	 */
	std::string GetFullMessage(const std::string& rstrLinePrefix = "") const;

protected :
	/*!
	 * \brief Positionne le message de cette exception.
	 *
	 * \param [in] rstrMessage Le message.
	 */
	void SetMessage(const std::string& rstrMessage);
	
	/*!
	 * \brief Ajoute une cause à cette description.
	 *
	 * \param [in] rstrCause La cause à ajouter à cette exception.
	 */
	void AddCause(const std::string& rstrCause);

	/*!
	 * \brief Ajoute un paramètre utile à connaître pour décrire cette exception.
	 *
	 * \param [in] rstrKey La clé du paramètre.
	 * \param [in] rtValue La valeur du paramètre.
	 */
	template<class T> void AddParam(const std::string& rstrKey, const T& rtValue);

	/*!
	 * \brief Ajoute une résolution à cette exception.
	 *
	 * \param [in] rstrResolution La résolution à ajouter.
	 */
	void AddResolution(const std::string& rstrResolution);

private:
	std::string m_strMessage; /*<! Le message descriptif de cette exception */
	const CException* m_poOriginException; /*!< L'exception à l'origine de celle-ci. */

	typedef std::vector<std::string> TVecCauses; /*!< Type pour stocker les causes. */
	TVecCauses m_oVecCauses; /*!< La liste des causes possibles de cette exception. */

	typedef std::vector<std::string> TVecResolutions; /*!< Type pour stocker les résolutions. */
	TVecResolutions m_oVecResolutions; /*!< La liste des résolutions possibles. */

	typedef std::map<std::string, std::string> TMapParams; /*!< Type pour mapper les paramètres et leurs valeurs. */
	TMapParams m_oMapParams; /*!< La liste des paramètres additionnels. */
};


Le rapport pourra être généré alors de cette ainsi, dans la fonction GetFullMessage()

 
Sélectionnez
std::string CException::GetFullMessage(const std::string& rstrLinePrefix) const
{
	std::string sWhat = "";
	sWhat += rstrLinePrefix;
	sWhat += "--> " + m_strMessage;
	sWhat += "\n";
	if(m_oVecCauses.size() > 0)
	{
		sWhat += rstrLinePrefix;

		if(m_oVecCauses.size() > 1)
			sWhat += "\t*Causes possibles :\n";
		else
			sWhat += "\t*Cause possible :\n";

		for each(std::string sCauseIt in m_oVecCauses)
		{
			sWhat += rstrLinePrefix;
			sWhat += "\t\t- ";
			sWhat += sCauseIt + "\n";
		}
	}
	if(m_oVecResolutions.size() > 0)
	{
		sWhat += rstrLinePrefix;

		if(m_oVecResolutions.size() > 1)
			sWhat += "\t*Résolutions possibles :\n";
		else
			sWhat += "\t*Résolution possible :\n";

		for each(std::string sResolutionIt in m_oVecResolutions)
		{
			sWhat += rstrLinePrefix;
			sWhat += "\t\t- ";
			sWhat += sResolutionIt + "\n";
		}
	}
	if(m_oMapParams.size() > 0)
	{
		sWhat += rstrLinePrefix;

		if(m_oMapParams.size() > 1)
			sWhat += "\t*Paramètres :\n";
		else
			sWhat += "\t*Paramètre :\n";

		for each(const std::pair<std::string, std::string> oParamIt in m_oMapParams)
		{
			sWhat += rstrLinePrefix;
			sWhat += "\t\t- ";
			sWhat += oParamIt.first + " : " + oParamIt.second + "\n";
		}
	}
	if(m_poOriginException != 0)
	{
		sWhat += rstrLinePrefix;
		sWhat += "\t*Origine :\n";
		sWhat += rstrLinePrefix;
		sWhat += m_poOriginException->GetFullMessage("\t\t");
	}

	return sWhat;
}


Puis une exception particulière :

 
Sélectionnez
/*!
 * \brief Exception levée lorsque l'accès au fichier de log de l'utilisation de la mémoire présente un problème.
 */
class CFileAccessException : public CException
{
public:
	/*!
	 * \brief Constructeur.
	 *
	 * \param [in] rstrFilename Le nom du fichier dont l'accès a échouée.
	 * \param [in] poOriginException L'éventuelle exception à l'origine de celle-ci.
	 */
	CFileAccessException(
		const std::string& rstrFilename,
		const CException* poOriginException = 0)
	{
		SetMessage("Impossible d'accéder au fichier");

		AddCause("Le nom du fichier est incorrect.");
		AddCause("Le fichier est verrouillé.");
		AddCause("Vous n'avez pas les droits d'accéder à ce fichier.");

		AddResolution("Essayez de l'ouvrir manuellement");

		AddParam("Nom du fichier", rstrFilename);
	}
};

Désormais, chaque classe pourra être livrée avec ses propres "problèmes potentiels" sous forme d'exception.

III-B. La gestion de la mémoire

Cet partie reprend ce tutoriel auquel les fonctions malloc, calloc, realloc et free on été rajoutées.

S'il y a bien quelque chose de facile à faire en C++, c'est de créer des fuites de mémoire.
Cette bibliothèque ayant pour vocation de gérer des centaines de milliers de données, une libération de mémoire oubliée peut poser de sérieux problème à l'exécution mais surtout lors du débogage si nous n'avons pas vraiment d'informations permettant de cibler le problème facilement et par conséquent rapidement.

III-B-1. La technique

Le but du jeu est d'intercepter toute allocation/libération dynamique faite durant l'exécution d'un programme pour les comptabiliser, avant de les rediriger vers la CRT comme si de rien n'était.
A la fin de l'exécution, nous envoyons le différentiel dans un fichier avec des informations sur l'endroit où se trouvent l'allocation qui n'a pas été supprimée (fichier + ligne et éventuellement fonction).
Bien entendu, cela doit rester transparent pour l'utilisateur, en d'autres terme il ne devra pas à avoir à taper ce genre de code :

 
Sélectionnez
CBook* pBook = 0;
#ifdef _DEBUG
pBook = SafeNew(CBook);
#else
pBook = new CBook();
#endif // _DEBUG

...

#ifdef _DEBUG
SafeDelete(pBook);
#else
delete pBook
#endif // _DEBUG

Le meilleur moyen d'intercepter ces opérations sur la mémoire est de redéfinir les opérateurs/fonctions standards disponibles à l'utilisateur, via des macros utilisant nos propres opérateurs/fonction qui eux, redirigeront la demande vers notre gestionnaire.

III-B-2. Les différents moyen d'allouer/libérer une ressource

Allocation Désallocation
new delete
new[] delete[]
malloc/calloc/realloc free


En factorisant les fonctionnalités, nous obtenons ce tableau :

Opérateur/Fonction standard Redirection dans le gestionnaire
new
new[]
malloc
calloc
Alloc
realloc Realloc
delete
delete[]
free
ToDelete / Free

III-B-2_1. Le type d'allocation

Comme nous souhaitons préserver les éventuelles erreurs du développeur dans l'utilisation de la mémoire, pour chaque bloc alloué nous devons les "typer" selon la manière dont ils ont été alloués.

 
Sélectionnez
/*!
 * Enumère les types de manipulation de la mémoire possibles.
 */
enum E_ALLOC_TYPE
{
	E_OPERATOR, /*< Par les opérateurs new et delete. */
	E_OPERATOR_ARRAY, /*!< Par les opérateurs new[] et delete[]. */
	E_C_STYLE /*!< Par les fonctions malloc, calloc, realloc et free. */
};

III-B-2_2. Le block de mémoire

Pour chaque allocation, nous allons stocker des informations sur le bloc alloué comme la taille, le type ainsi que le lieu de la demande.

 
Sélectionnez
/*!
 * \struct SBlock
 * \brief Représente un bloc de mémoire.
 */
struct SMemBlock
{
	std::size_t m_stSize; /*!< Taille allouée. */
	std::string m_strFile; /*!< Fichier source  ce bloc a été alloué. */
	unsigned int m_uiLine; /*!< Numéro de ligne du fichier source  ce bloc a été alloué. */
	std::string m_strFunction; /*!< Le nom de la fonction qui a alloué ce bloc. */
	E_ALLOC_TYPE m_eType; /*!< Indique si ce bloc est un tableau ou pas. */
};

III-B-2_3. L'allocation

Pour allouer un block, nous créons une fonction d'allocation avec toutes les informations requises pour caractériser ce block. Nous ajoutons également un flags de mise à 0 de la mémoire pour gérer calloc ainsi qu'un autre pour indiquer s'il faut gérer ce bloc ou pas.

 
Sélectionnez
/*!
 * \brief Alloue un bloc de mémoire.
 *
 * \param [in] stSize La taille à allouer (en octet).
 * \param [in] rstrFile Le fichier dans lequel a été réclamé l'allocation.
 * \param [in] uiLine La ligne du fichier source  a été réclamé l'allocation.
 * \param [in] rstrFunction Le nom de la fonction
 * \param [in] eType Le type d'allocation.
 * \param [in] bManage Indique si le gestionnaire de mémoire doit gérer cette allocation.
 * \param [in] bSetMemory Indique s'il faut initialiser la mémoire avec des 0.
 * \return Un pointeur sur la zone allouée.
 */
void* Alloc(std::size_t stSize,
		const std::string& rstrFile,
		unsigned int uiLine,
		const std::string& rstrFunction,
		E_ALLOC_TYPE eType,
		bool bManage = true,
		bool bSetMemory = false);

Cette fonction commence par allouer la mémoire, grâce à malloc ou calloc si la demande de mise à 0 a été positionnée. Puis elle construit le bloc grâce aux paramètres données afin de le stocker dans un dictionnaire référençant le pointeur avec ce bloc. Enfin, elle renverra un pointeur vers l'espace mémoire alloué.

III-B-2_4. La réallocation

Nous créons également une fonction de réallocation.

 
Sélectionnez
/*!
 * \brief Réalloue un bloc de mémoire.
 *
 * \param [in] pvPointer Le pointeur vers la mémoire à réallouer.
 * \param [in] stSize La taille à réallouer (en octet).
 * \param [in] rstrFile Le fichier dans lequel a été réclamé la réallocation.
 * \param [in] uiLine La ligne du fichier source  a été réclamé la réallocation.
 * \param [in] rstrFunction Le nom de la fonction
 * \return Un pointeur sur la zone réallouée.
 */
void* Realloc(void* pvPointer,
			std::size_t stSize,
			const std::string& rstrFile,
			unsigned int uiLine,
			const std::string& rstrFunction);

Cette fonction ira chercher le bloc initialement alloué. Puis, elle appellera realloc avant de mettre à jour le pointeur ainsi que la taille allouée.

III-B-2_5. La libération

La libération est un peu spéciale, elle se décompose en deux temps. En effet, nous ne pouvons pas rediriger les opérateurs delete et delete[] avec les informations sur le fichier, la ligne ou la fonction de la demande.
Nous devons alors posséder une pile de libérations à remplir en amont avant d'appeler la fonction qui libèrera finalement la mémoire.

 
Sélectionnez
/*!
 * \brief Stock les infos sur le prochain bloc à libérer.
 *
 * \param [in] rstrFile Le fichier dans lequel a été réclamé l'allocation.
 * \param [in] uiLine La ligne du fichier source  a été réclamé l'allocation.
 * \param [in] rstrFunction La fonction.
 */
void ToDelete(const std::string& rstrFile, unsigned int uiLine, const std::string& rstrFunction);

/*!
 * \brief Libère un bloc en mmoire.
 *
 * \param [in] pvPointer Le pointeur vers la zone à libérer.
 * \param [in] eType Le type de libération.
 */
void Free(void* pvPointer, E_ALLOC_TYPE eType);

III-B-2_6. Les autres fonctions

Nous proposons également une fonction qui renvoi la taille actuellement allouée par nos données dans l'application.

 
Sélectionnez
/*!
 * \brief Obtient la quantité de mémoire actuellement allouée par ce gestionnaire.
 *
 * \return La quantité de mémoire actuellement allouée par ce gestionnaire.
 */
unsigned long GetAllowedSize();

Le gestionnaire, qui possède ces méthodes, devra être unique. Nous passerons par une variable statique, le concept de Singleton est détaillé plus tard.

 
Sélectionnez
/*!
 * \brief Obtient l'instance unique de cette classe.
 *
 * \return L'instance unique de cette classe.
 */
static CMemoryManager& GetInstance();

// Implémentation
CMemoryManager& CMemoryManager::GetInstance()
{
	static CMemoryManager oInstance;

	return oInstance;
}

III-B-2_7. Les exceptions possibles

Les problèmes ci dessous donneront lieu à un lancement d'exception :

Problème Exception
Echec d'ouverture du fichier de log CMemoryFileException
Echec d'allocation/réallocation CAllocException
La méthode de libération est différente de celle de l'allocation CBadFreeException
Tentative de réallocation d'un pointeur non créé avec malloc/calloc CBadReallocException

III-B-2_8. Le rapport de fuite

En fin d'exécution du programme (lorsque la variable statique se détruit), les blocs restants sont des fuites. Nous les indiquerons dans un fichier de log.

 
Sélectionnez
void CMemoryManager::~CMemoryManager()
{
	if(m_mapAllouedBlock.size() > 0)
		ReportLeaks();
	else
		// pas de fuite
}

void CMemoryManager::ReportLeaks()
{
	// Parcourt des blocks restant
	// Inscription des infos dans le fichier de log
}

III-B-2_9. La redirection des opérateurs/fonctions standards

Notre gestionnaire est prêt à l'emploi, il ne reste plus qu'à redéfinir les différents moyens d'allouer/libérer la mémoire.
Nous utiliseront des macros. Un fichier sera à inclure en début de notre code pour activer le gestionnaire, un autre pour le désactiver. Commençons par redéfinir les opérateurs

 
Sélectionnez
inline void* operator new(std::size_t stSize, const char* szFilename, unsigned int uiLine, const char* sFunction)
{
	return MemMgr.Alloc(stSize, szFilename, uiLine, sFunction, kin::CMemoryManager::E_OPERATOR);
}
inline void* operator new[](std::size_t stSize, const char* szFilename, unsigned int uiLine, const char* sFunction)
{
	return MemMgr.Alloc(stSize, szFilename, uiLine, sFunction, kin::CMemoryManager::E_OPERATOR_ARRAY);
}

En redéfinissant les opérateurs new/new[] de cette manière, le compilateur nous réclamera de redéfinir les opérateur delete et delete[] respectant la même signature

 
Sélectionnez
inline void operator delete(void* pvPointer, const char* szFilename, unsigned int uiLine, const char* sFunction) throw()
{
	MemMgr.ToDelete(szFilename, uiLine, sFunction);
	MemMgr.Free(pvPointer, kin::CMemoryManager::E_OPERATOR);
}
inline void operator delete[](void* pvPointer, const char* szFilename, unsigned int uiLine, const char* sFunction) throw()
{
	MemMgr.ToDelete(szFilename, uiLine, sFunction);
	MemMgr.Free(pvPointer, kin::CMemoryManager::E_OPERATOR_ARRAY);
}

Il faut également redéfinir les opérateurs de libération avec leur signature standard.

 
Sélectionnez
inline void operator delete(void* pvPointer) throw()
{
    MemMgr.Free(pvPointer, kin::CMemoryManager::E_OPERATOR);
}
inline void operator delete[](void* pvPointer) throw()
{
    MemMgr.Free(pvPointer, kin::CMemoryManager::E_OPERATOR_ARRAY);
}

Et enfin les macros de redirection pour utiliser nos opérateurs ou rediriger les fonctions standards vers notre gestionnaire.

 
Sélectionnez
#define new			new(__FILE__, __LINE__, __FUNCTION__)
#define delete			MemMgr.ToDelete(__FILE__, __LINE__, __FUNCTION__), delete
#define	malloc(sz)		MemMgr.Alloc(sz, __FILE__, __LINE__, __FUNCTION__, kin::CMemoryManager::E_C_STYLE);
#define	calloc(nb, sz)		MemMgr.Alloc(sz * nb, __FILE__, __LINE__, __FUNCTION__, kin::CMemoryManager::E_C_STYLE, true);
#define	realloc(ptr,sz)		MemMgr.Realloc(ptr, sz, __FILE__, __LINE__, __FUNCTION__);
#define	free(ptr)		MemMgr.ToDelete(__FILE__, __LINE__, __FUNCTION__), MemMgr.Free(ptr, kin::CMemoryManager::E_C_STYLE)

Puis les suppressions de macro pour désactiver le gestionnaire

 
Sélectionnez
#undef new
#undef delete
#undef malloc
#undef calloc
#undef realloc
#undef free

III-B-2_10. Utilisation et rendu

Pour l'utiliser, il faut activer le gestionnaire, c'est à dire inclure le fichier de redéfinition des macros en début de code, puis celui de désactivation en fin de code.

 
Sélectionnez
#include <kinUtils/Memory/EnableMemoryManager.h>

int main()
{
	int* i1 = new int(3);
	int* i2 = new int[3];
	int* i3 = (int*)malloc(2 * sizeof(int));
	int* i4 = (int*)realloc(i3, sizeof(int));
	int* i5 = (int*)calloc(2, sizeof(int));

	delete i1;
	delete[] i2;
	free(i5);
}

#include <kinUtils/Memory/DisableMemoryManager.h>

Ce code ci dessus nous amènera à ce rapport :

 
Sélectionnez
----------- Rapport de mémoire -----------

++ Allocation      | 0x008867B0 |         4 octets | kintestapp.cpp | main (6)
++ Allocation      | 0x00886928 |        12 octets | kintestapp.cpp | main (7)
++ Allocation      | 0x00886970 |         8 octets | kintestapp.cpp | main (8)
++ Réallocation    | 0x00886970 |        -4 octets | kintestapp.cpp | main (9)
++ Allocation      | 0x00886CE0 |         8 octets | kintestapp.cpp | main (10)
-- Désallocation   | 0x008867B0 |         4 octets | kintestapp.cpp | main (12)
-- Désallocation   | 0x00886928 |        12 octets | kintestapp.cpp | main (13)
-- Désallocation   | 0x00886CE0 |         8 octets | kintestapp.cpp | main (14)

----------- Fuite(s) détectée(s) -----------

-> 0x00886970 |         4 octets | kintestapp.cpp | main (8)

!! 1 bloc non-libéré 4 octets !!

Si nous allouons un bloc de mémoire via le gestionnaire et que nous libérons correctement la mémoire mais dans une portion de code où le gestionnaire n'est plus actif, cela créera de fuites fictives.
Par conséquent, tout comme les anti inclusions multiples, il faut avoir le réflex de l'activer en début de code et le désactiver en fin de code.

III-C. Le singleton

Le singleton est un Design Pattern permettant de garantir l'unicité de l'instance d'une classe. En général ce sont des gestionnaires (de log, de connexion, de ressources etc.). Pour assurer ceci, il y a deux manière :

  • Utiliser un membre statique comme nous l'avons fait pour le gestionnaire de mémoire et le renvoyer via une méthode
  • Utiliser un pointeur statique initialisé à 0. L'instance sera construite au premier appel

Le constructeur sera donc privé.
Avantages et inconvénients de ces deux méthodes :

Type d'instance Avantages Inconvénients
Instance statique Nous ne nous soucions pas de la destruction Nous ne contrôlons pas la destruction
Instance dynamique Nous décidons quand et dans quel ordre sont détruits les singletons L'utilisateur doit veiller à bien détruire les singletons, dans le bon ordre


Nous utiliseront donc la seconde version, pour avoir le contrôle des destructions. En voici les principes :

Nous avons utilisé la méthode du membre statique dans le gestionnaire de mémoire étant donné qu'il n'était pas possible de brancher le gestionnaire de mémoire sur lui même sous peine de créer des inclusions récursives.

Voyons un premier jet de cette solution :

 
Sélectionnez
/*!
 * \brief Un gestionnaire.
 */
class CMyManager
{
public:
	/*!
	 * \brief Récupère l'instance.
	 *
	 * \return L'instance.
	 */
	static CMyManager& GetInstance();
	
	/*!
	 * \brief Détruit l'instance.
	 */
	static void Destroy();
	
private:
	/*!
	 * \brief Constructeur privé.
	 */
	CMyManager();
	
	/*!
	 * \brief Destructeur privé.
	 */
	~CMyManager();
	
	static CMyManager* s_pInstance; /*!< Instance unique. */
};

static CMyManager* CMyManager::s_pInstance = 0;

CMyManager::CMyManager() {}
CMyManager::~CMyManager() {}

CMyManager& CMyManager::GetInstance()
{
	if(!s_pInstance)
		s_pInstance = new CMyManager();
	return s_pInstance;
}

void CMyManager::Destroy()
{
	if(s_pInstance)
	{
		delete s_pInstance;
		s_pInstance = 0;
	}
}

Naturellement nous n'allons pas répéter cette mécanique à chaque fois que nous aurons besoin de faire un Singleton. Nous devons construire une classe de base qui s'en chargera.
Comme nous ne connaissons pas le type qui héritera de notre classe, nous la templarisons. Ainsi, le pointeur de l'instance sera de type T et GetInstance() renverra du T&.

 
Sélectionnez
/*!
 * \brief Classe de base de tous les singletons.
 */
template<class T>
class CSingleton
{
public:
	/*!
	 * \brief Renvoi l'instance de la classe.
	 *
	 * \return L'instance unique de cette classe.
	 */
	static T& Instance();

	/*!
	 * \brief Détruit l'instance de la classe.
	 */
	static void Destroy();

protected :
	/*!
	 * \brief Constructeur.
	 */
	CSingleton();

	/*!
	 * \brief Destructeur.
	 */
	~CSingleton();

private:
	static T* s_pInstance; /*!< L'instance (unique) de cette classe. */
};

Définissons deux macros d'initialisation pour rendre les "formalités" plus simples :

  • L'une permettant de rendre amis le gestionnaire avec sa classe de base, le singleton. Ceci permettra d'autoriser CSingleton à accéder au constructeur privé du gestionnaire.
  • L'autre permettant d'initialiser le pointeur statique du singleton.
 
Sélectionnez
/*!
 * \brief Donne l'accès au singleton à la construction du type encapsulé.
 */
#define SINGLETON_DECLARE(Class) \
	public : \
	friend class kin::CSingleton<Class>; \
		static Class& GetInstance(); \
	private:

/*!
 * \brief Initialise un singleton.
 */
#define SINGLETON_IMPL(Class) \
template <> Class* kin::CSingleton<Class>::ms_ptInstance = 0; \
	Class& Class::GetInstance() \
	{ \
		return kin::CSingleton<Class>::GetInstance(); \
	} /*!< Initialise le pointeur static d'un singleton. */

Il nous suffit alors d'étendre cette classe pour obtenir un singleton.

 
Sélectionnez
class CMyManager : public CSingleton<CMyManager>
{
SINGLETON_DECLARE(CMyManager)
...
};

// Dans un .cpp
SINGLETON_IMPL(CMyManager)

III-D. La gestion des logs

Cette partie a été reprise de cet article en y ajoutant la possibilité de brancher des logger "externes" à l'unité logiciel. Notre librairie finale passera en DLL, le programme voudra peut-être rediriger les logs de cette bibliothèque sans sa gestion.

La plupart des logiciels possède une gestion de logs permettant de tracer ce qui se passe durant son exécution, et ainsi avoir une base d'analyse pour différents cas (dysfonctionnement, temps etc.). Voici les fonctionnalités que nous nous apprêtons à réaliser :

  • Etre utilisable via les opérateurs de flux, pour obtenir un code plus propre et pratique.
  • Pouvoir inscrire un message composé de chaîne de caractères, entier, flottant ou objets métier sans passer par des conversions en chaîne au préalable, cela ne serait pas productif et alourdirait le code utilisateur.
  • Pouvoir facilement créer et brancher une destination de log. Où vont aller ces messages ? Dans un fichiers ? Texte, XML ? Ou une base de données, un socket etc. ? Brancher un nouveau "logger" doit être simple, sans en changer l'utilisation du gestionnaire.

III-D-1. Les loggers

Nous allons utiliser le patron de méthode. Ce Design Pattern consiste à créer une classe de base qui fera appel à une ou plusieurs méthode(s) virtuelle(s) afin de proposer un comportement commun de tous les loggers tout en respectant leur spécificité. Ici, chaque logger se différencie par sa méthode d'écriture. L'un écrira dans un fichier, un autre sur la console, un troisième sur une fenêtre de Debug, puis pourquoi pas dans un socket où sur une partie de l'IHM etc. L'avantage est que pour créer un nouveau logger, une seule méthode, bien ciblée et spécifique, sera à implémenter.

 
Sélectionnez
class CLogger
{
private:
	virtual void Write(const std::string& rstrMessage) = 0;
};

Ceci fait, nous pouvons nous appuyer sur cette méthode en interne pour proposer... du confort avec l'opérateur de flux d'insertion "<<".

 
Sélectionnez
/*!
 * \brief Opérateur d'insertion dans un flux.
 *
 * \param [in] rtToLog Le T à logger.
 */
template <class T> ILogger& ILogger::operator <<(const T& rtToLog)
{
	std::ostringstream Stream;
	Stream << rtToLog;
	Write(Stream.str());

	return *this; // Nous renvoyons le logger pour pouvoir cumuler les appels à <<
}

Exemple de logger qui inscrit les message dans la sortie Debug de l'IDE :

 
Sélectionnez
class CDebugLogger : public CLogger
{
private:
	virtual void Write(const std::string& rstrMessage);
};

void CDebugLogger::Write(const std::string& rstrMessage)
{
	OutputDebugString(rstrMessage.c_str());
}

CDebugLogger logger;
int i = 0;
float f = 0.;
logger << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;

III-D-2. Le gestionnaire de logs

Nous allons sûrement utiliser plusieurs loggers.

 
Sélectionnez
class CFileLogger : public CLogger { ... };
class CConsoleLogger : public CLogger { ... };
class CSocketLogger : public CLogger { ... };
// etc...

Il serait bien dommage d'imposer à l'utilisateur ce genre de code

 
Sélectionnez
g_fileLogger << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;
g_socketLogger << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;
#ifdef _DEBUG
	g_debugLogger << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;
	g_consoleLogger << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;
#endif // <i>DEBUG

Nous devons donc créer un gestionnaire (donc Singleton) qui va encapsuler les loggers à prendre en compte. En fait, le gestionnaire de log est un simple logger, qui tient une liste de loggers. Sa méthode Write ne fera que transmettre le message aux loggers "branchés" dessus.

 
Sélectionnez
/*!
 * \brief Gestionnaire de log.
 */
class CLogManager : public CSingleton<CLogManager>, public ILogger
{
SINGLETON_DECLARE(CLogManager);

public:
	/*!
	 * \brief Branche un logger à ce gestionnaire.
	 *
	 * \param pLogger Le logger.
	 * \param bExtern Indique si le logger est extern à l'instance applicative.
	 */
	void AddLogger(ILogger* pLogger, bool bExtern = false);

private:
	/*!
	 * \brief Constructeur.
	 */
	CLogManager();

	/*!
	 * \brief Destructeur.
	 */
	virtual ~CLogManager();

	/*!
	 * \brief Log un message.
	 *
	 * \param [in] rstrMessage Le message à logger.
	 */
	void Write(const std::string& rstrMessage);

	typedef std::map<ILogger*, bool> TMapLoggers; /*!< Type de stockage des loggers internes. */
	TMapLoggers m_mapLoggers; /*!< La liste des loggers enregistrés. */
};

#define Log CLogManager::GetInstance() // Petit racourcit pratique.

A la destruction, le gestionnaire supprimera les loggers enregistrés. Mais comme nous allons faire une DLL, le gestionnaire possèdera des loggers issus d'une autre unitaire logicielle (notre .exe). Par conséquent, à l'enregistrement des loggers, nous rajoutons un flag indiquant si le logger est externe ou pas. S'il l'est, le gestionnaire ne le détruira pas.
Voyons comment l'initialisation se passe :

 
Sélectionnez
Log.AddLogger(new CFileLogger("kin.log"));
Log.AddLogger(new CSocketLogger("pc-de-control", 3159));

// Si nous sommes en debug, nous branchons aussi le cadre de sortie de l'IDE, ainsi que la console.
#ifdef _DEBUG
	Log.AddLogger(new CConsoleLogger());
	Log.AddLogger(new CDebugLogger());
#endif // DEBUG

Enfin, voici comment s'utilisera ce gestionnaire

 
Sélectionnez
Log << "Un message de log, nous y mettons un entier " << i << " puis un flottant " << f;

"UMLement" parlant, nous avons codé ceci :

Image non disponible


Nous verrons qu'en fin d'article, une fois le passage en DLL effectué, nous pourrons faire ceci :

 
Sélectionnez
Log.AddLogger(new CFileLogger("kin.log"));
Log.AddLogger(new CSocketLogger("pc-de-control", 3159));

#ifdef _DEBUG
	Log.AddLogger(new CConsoleLogger());
	Log.AddLogger(new CDebugLogger());
#endif // DEBUG

// Redirection des messages de la DLL dans ceux de l'exe.
kinAddLogger(Log); // Log, le gestionnaire étant lui même un logger, ça passe.

Par la suite, pourquoi ne pas préfixer les messages des différentes unités ? Pour obtenir par exemple

 
Sélectionnez
kinTestApp - Début d'exécution
kinBusiness - Chargement des clients, 2884 données, 23 ms
kinTestApp - Fin d'exécution

III-D-3. Convertisseurs chaînes/types

Les classes ci dessous ont été reprises du moteur graphique de loulou.

Pour transformer tout type de donnée en chaîne de caractères et inversement, le plus pratique est de passer par les flux de chaînes, produisant ce genre de code :

 
Sélectionnez
int i; float f;

// Construction d'une chaîne
std::ostringstream ostream;
steam << i << f;
std::string strString = stream.str();

// Extraction des valeur depuis la chaîne.
std::istringstream istream(strString);
istream >> f >> i;

Nous allons encapsuler ces conversions dans des objets, puis leur donner plus de clarté d'utilisation. Commençons par le constructeur de chaîne. Par défaut, il est vide mais nous laissons la possibilité de l'initialiser avec une valeur. Par la suite, nous pourrons le remplir d'avantage via d'autres valeurs. Et enfin, nous récupèrerons la chaîne résultante.

 
Sélectionnez
/*!
 * \brief Constructeur de chaîne à partir de tout type de données.
 */
class CStringBuilder
{
public :
	/*!
	 * \brief Constructeur.
	 */
	CStringBuilder();

	/*!
	 * \brief Constructeur avec construction initiale.
	 *
	 * \tparam T Le type de la donnée à partir de laquelle initialiser cette instance.
	 * \param [in] rtValue La valeur initiale.
	 */
	template <typename T> CStringBuilder(const T& rtValue);

	/*!
	 * \brief Alimente ce constructeur de chaîne.
	 *
	 * \tparam T Le type de la donnée source.
	 * \param [in] rtValue La valeur à ajouter à ce constructeur de chaîne.
	 */
	template <typename T> inline CStringBuilder& operator ()(const T& rtValue);
	
	/*!
	 * \brief Opérateur de conversion en chaîne de caractère.
	 *
	 * \return La chaîne construite.
	 */
	inline operator std::string();

private:
	std::ostringstream m_oOutStream; /*!< Le flux de sortie interne. */
};

Voici l'implémentation des méthodes importantes

 
Sélectionnez
template <typename T>
inline CStringBuilder::CStringBuilder(const T& rtValue)
{
	m_oOutStream << rtValue;
}

template <typename T>
inline CStringBuilder& CStringBuilder::operator ()(const T& rtValue)
{
	m_oOutStream << rtValue;

	return *this;
}

inline CStringBuilder::operator std::string()
{
	return m_oOutStream.str();
}

Quand à l'utilisation, elle se résume à ceci

 
Sélectionnez
CStringBuilder builder; int i = 2; float f = 3.4;
std::string str = builder(i)(f);

Intéressons nous maintenant à l'extracteur. Le principe est le même, il encapsule un flux d'entrée de chaînes puis propose des facilités d'utilisation pour reconstruire les données dans leur type.

 
Sélectionnez
/*!
 * \brief Extracteur de chaînes dans un type choisit.
 */
class CStringExtractor
{
public :
	/*!
	 * \brief Constructeur.
	 *
	 * \param [in] rstrText La chaîne initiale.
	 */
	CStringExtractor(const std::string& rstrText = "");

	/*!
	 * \brief Remplit une valeur.
	 *
	 * \tparam T Le type de la valeur à templir.
	 * \param [in] rtValue La valeur à remplir.
	 */
	template <typename T> CStringExtractor& operator ()(T& rtValue);

	/*!
	 * \brief Indique si cet extracteur est vide ou non.
	 */
	bool Empty();

	/*!
	 * \brief Réalimente cet extracteur.
	 *
	 * \param [in] rstrText Le texte avec lequel réalimenter cet extracteur.
	 */
	void Push(const std::string& rstrText);

private :
	std::istringstream m_oInStream; /*!< Le lfux d'entrée interne. */
};

Voilà l'implémentation

 
Sélectionnez
template <typename T>
inline CStringExtractor& CStringExtractor::operator ()(T& rtValue)
{
	if (m_oInStream.str().size() == 0)
		return *this;

	if (!(m_oInStream >> rtValue))
	{
		if (m_oInStream.eof())
			throw new CExtractorConvertException(typeid(T).name());
		else
			throw new CExtractorEmptyException(typeid(T).name());
	}

	return *this;
}

inline bool CStringExtractor::Empty()
{
	return m_oInStream.eof();
}

inline void CStringExtractor::Push(const std::string& rstrText)
{
	m_oInStream.clear();
	m_oInStream.str(rstrText.c_str());
}

Et l'utilisation :

 
Sélectionnez
CStringExtractor oExtractor("2 3,4");
int i = 0; float f = 0.;
oExtractor(i)(f);

III-E. Les outils externes

Nous allons également utiliser des outils externes, autant les installer de suite.

III-E-1. libxml2

Pour manipuler les fichiers XML, nous utilisons libxml2, un peu moins performante que xerces-c, mais plus facile à utiliser. Téléchargez la bibliothèque à l'adresse ftp://fr.rpmfind.net/pub/libxml/. Allez dans le répertoire "win32" puis prenez les packages libxml2 (dernière version), iconv et zlib. Décompressez tout ça dans votre répertoire de libs externes puis indiquez à votre compilateur, pour chacune d'entre elle les répertoires include et lib de ces bibliothèques.

III-E-2. boost

Boost est un ensemble de bibliothèques utilitaires, performantes et portables dont il est difficile de passer à côté. Voyons comment les installer.
Commencez par télécharger la dernière version à l'adresse http://www.boost.org/users/download/ ainsi que Boost Jam, compilée (pour ma part ce sera ntx86). Décompressez le tout dans un même répertoire, puis ouvrez l'invite de commande de Visual Studio et dirigez vous dans le répertoire où vous avez mis les sources. Tapez ensuite "bjam toolset=msvc --build-type=complete install"

Image non disponible


Patientez désormais un petit peut, 3/4h selon votre machine, et vous obtiendrez un répertoire "C:/boost" contenant les sources ainsi que les libs compilées. Placez le dans votre répertoire de libs, puis indiquez à votre éditeur les sous-répertoire include et lib produits.

Je compile tout pour être sûr qu'il ne manquera rien, mais pour plus de précisions sur l'installation et les options de compilation de boost : Installer et utiliser Boost/Boost.TR1 avec Visual C++.


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 © 2009 Aurélien FILEZ 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.