[16] La gestion de la mémoire

(Une partie de C++ FAQ Lite fr, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)

Traduit de l'anglais par Fabrice Clerc

Les FAQs de la section [16]


[16.1] Est-ce que delete p détruit le pointeur p, ou la donnée pointée *p?

La donnée pointée.

Le mot-clé devrait plutôt être delete_la_chose_sur_laquelle_pointe. On trouve le même abus de langage en C dans le cas du free: free(p) signifie en fait free_le_truc_sur_lequel_pointe(p).

[ Haut | Bas | Rechercher ]


[16.2] Peut-on faire un free() sur pointeur alloué par new? Peut-on faire un delete sur un pointeur alloué par malloc()?

Non!

Il est parfaitement légal, moral, et tout à fait sain d'utiliser malloc() et delete ou d'utiliser new et free() dans le même programme. Mais il est illégal, immoral, et totalement abject d'appeler free() sur un pointeur alloué par new, ou d'appeler delete sur un pointeur alloué par malloc().

Attention! Je reçois de temps en temps des e-mails de gens qui me disent que ça ne pose pas de problèmes sur la machine X avec le compilateur Y. Ce n'est pas pour ça que c'est correct! Les gens disent aussi parfois: "Mais c'est juste un tableau de char." Il n'en reste, il ne faut pas mélanger malloc() et delete, ou new et free(), sur le même pointeur! Si vous avez fait une allocation en utilisant p = new char[n], vous devez utiliser delete[] p; vous ne devez surtout pas utiliser free(p). De façon similaire, si vous avez fait une allocation en utilisant p = malloc(n), vous devez utiliser free(p); vous ne devez surtout pas utiliser delete[] p ou delete p! Mélanger ces appels pourrait engendrer une catastrophe à l'exécution si le code doit être porté sur une nouvelle machine, un nouveau compilateur, voire même sur une nouvelle version du même compilateur.

Vous aurez été prévenus.

[ Haut | Bas | Rechercher ]


[16.3] Pourquoi utiliser new plutôt que ce bon vieux malloc()?

Pour les constructeurs et destructeurs, pour un typage plus sûr, parce qu'il est redéfinissable.

[ Haut | Bas | Rechercher ]


[16.4] Peut-on faire un realloc() sur un pointeur alloué par new?

Non!

Quand realloc() doit copier la zone allouée, il utilise une copie bit-à-bit (bitwise), ce qui mettra en lambeaux de nombreux objets C++. On doit laisser aux objets C++ le soin de se copier eux-mêmes. Ils utilisent pour cela leur constructeur de copie ou leur opérateur d'affectation.

Qui plus est, le tas (heap) utilisé par new pourrait très bien ne pas être le même que le tas utilisé par malloc() et par realloc()!

[ Haut | Bas | Rechercher ]


[16.5] Doit-on vérifier que p n'est pas NULL après un p = new Fred()?

Non! (Mais si vous avez un vieux compilateur, vous devrez peut-être forcer le compilateur à se comporter ainsi).

Il est vraiment galère d'avoir à tester explicitement le pointeur NULL après chaque allocation par new. Devoir écrire du code comme celui ci-dessous est plutôt pénible:

Fred* p = new Fred();
if (p == NULL)
throw bad_alloc();

Et c'est encore plus pénible si votre compilateur ne supporte pas les exceptions (ou si vous ne voulez pas les utiliser):

Fred* p = new Fred();
if (p == NULL) {
cerr << "Couldn't allocate memory for a Fred" << endl;
abort();
}

Mais heureusement, si à l'exécution le système ne peut pas allouer sizeof(Fred) octets lors d'une instruction p = new Fred(), C++ lancera une exception bad_alloc. Contrairement à malloc(), new ne retourne jamais NULL!

écrivez-donc simplement:

Fred* p = new Fred(); // Pas la peine de vérifier si p vaut NULL

Attention, les vieux compilateurs ne supportent pas forcément ce mécanisme. Il faut que vous vérifiez si oui ou non votre compilateur le supporte, l'information se trouvant probablement dans la doc du compilateur, à la section qui parle de "new". Si vous avez un vieux compilateur, il est possible que vous deviez forcer le compilateur à se comporter ainsi.

[ Haut | Bas | Rechercher ]


[16.6] Comment forcer un (vieux) compilateur à faire en sorte que new ne renvoie pas NULL? UPDATED!
[Ajout récent (10/99): main() a maintenant un type de retour. Les FAQs modifiées récemment sont "chaînées". Cliquez ici pour aller à la suivante.]

ça sera sans doute fait de façon automatique dans une prochaine version du compilateur en question.

Mais si votre vieux compilateur ne fait pas automagiquement le test de comparaison avec NULL, il est possible de forcer le système d'exécution (runtime system) à le faire en spécifiant une fonction de "new handler" (to handle en anglais veut dire dans ce contexte: se charger de, s'occuper de). Cette fonction "new handler" peut se comporter comme vous le souhaitez, elle peut par exemple afficher un message et appeler abort() pour terminer le programme, ou bien faire des delete sur certains objets et rendre la main à l'appelant (dans ce cas, operator new essaiera à nouveau d'allouer de la mémoire), ou encore lancer une exception grâce à throw, etc.

L'exemple suivant de "new handler" affiche un message et appelle abort(). Le "new handler" est spécifié en utilisant set_new_handler():

#include <new.h> // Pour avoir accès à set_new_handler
#include <stdlib.h>
// Pour avoir accès à abort()
#include <iostream.h>
// Pour avoir accès à cerr

void myNewHandler()
{
// C'est votre propre "new handler". Donnez-lui le comportement que vous voulez.
cerr << "Attempt to allocate memory failed!" << endl;
abort();
}

int main()
{
set_new_handler(myNewHandler);
// Permet de spécifier votre propre "new handler"
// ...
}

Après l'exécution de l'appel à set_new_handler(), l'operator new appelera votre fonction myNewHandler() s'il ne peut plus allouer de mémoire. Cela signifie que new ne renverra jamais NULL:

Fred* p = new Fred(); // Pas besoin de vérifier si p vaut NULL

Note: n'utilisez abort() qu'en dernier ressort. Si votre compilateur supporte la gestion des exceptions, vous devriez lancer une exception plutôt que d'appeler abort().

Note: si le constructeur d'un objet global ou statique alloue de la mémoire, new n'utilisera pas la fonction myNewHandler() dans la mesure où ce constructeur sera appelé avant l'exécution de main(). Il n'y a pas hélas de moyen simple de garantir que set_new_handler() soit appelée avant la première utilisation de new. Même si, par exemple, vous placez l'appel à set_new_handler() dans le constructeur d'un objet global, vous ne pourrez pas savoir si le module ("l'unité de compilation") qui contient cet objet global verra ses constructeurs exécutés en premier, en dernier, ou quelque part entre les deux. Et vous n'aurez donc pas la garantie que l'appel à set_new_handler() sera exécuté avant n'importe quel constructeur d'un objet global.

[ Haut | Bas | Rechercher ]


[16.7] Doit-on vérifier que p n'est pas NULL avant de faire un delete p?

Non!

Le langage C++ garantit que delete p n'a aucun n'effet si p vaut NULL. Dans la mesure où il y a un risque que vous vous trompiez en écrivant le test, et dans la mesure où la plupart des méthodologies de test vous obligent à tester effectivement tous les cas où il y a une branche, le mieux est de ne pas faire ce test if redondant.

Incorrect:

if (p != NULL)
delete p;

Correct:

delete p;

[ Haut | Bas | Rechercher ]


[16.8] Quelles sont les deux étapes d'un delete p?

L'opération delete p s'effectue en deux étapes: d'abord le destructeur est appelé, ensuite la mémoire est libérée. Le code généré pour un delete p ressemble en gros à ceci (si p est de type Fred*):

// Code original: delete p;
if (p != NULL) {
p->~Fred();
operator delete(p);
}

L'instruction p->~Fred() appelle le destructeur de l'objet Fred sur lequel pointe p.

L'instruction operator delete(p) appelle la primitive de désallocation de mémoire, void operator delete(void* p). Cette primitive est du même genre que free(void* p). (Notez cependant que ces deux primitives ne sont pas interchangeables; entre autres, il n'est même pas garantit qu'elles utilisent le même tas (heap)!).

[ Haut | Bas | Rechercher ]


[16.9] Lors d'un p = new Fred(), y-a-t-il une "fuite mémoire" si le constructeur de Fred lance une exception?

Non.

Si, dans l'instruction p = new Fred(), une exception survient lors de l'exécution du constructeur de Fred, le langage C++ garantit que les sizeof(Fred) octets de mémoire qui ont été alloués vont être automagiquement rendus au tas (heap).

Plus précisément, l'opération new Fred() s'effectue en deux étapes:

  1. sizeof(Fred) octets de mémoire sont alloués en utilisant la primitive void* operator new(size_t nbytes). Cette primitive est du même genre que malloc(size_t nbytes). (Notez cependant que ces deux primitives ne sont pas interchangeables; entre autres, il n'est même pas garantit qu'elles utilisent le même tas (heap)!).
  2. Un objet est construit dans cette zone mémoire, en appelant le constructeur de Fred. Le pointeur retourné par la première étape est passé au constructeur en tant que paramètre this. Cette deuxième étape est placée dans un bloc try ... catch, afin de pouvoir traiter le cas où une exception est lancée.

Le code qui sera effectivement généré ressemble en gros à ceci:

// Code original: Fred* p = new Fred();
Fred* p = (Fred*) operator new(sizeof(Fred));
try {
new(p) Fred();
// Placement new
} catch (...) {
operator delete(p);
// Désallouer la mémoire
throw;
// Relancer l'exception
}

L'instruction commentée comme étant un "Placement new " appelle le constructeur de Fred. Le pointeur p devient le pointeur this à l'intérieur du constructeur Fred::Fred().

[ Haut | Bas | Rechercher ]


[16.10] Comment allouer/désallouer un tableau d'objets?

Utilisez p = new T[n] et delete[] p:

Fred* p = new Fred[100];
// ...
delete[] p;

Quand vous avez alloué un tableau d'objets en utilisant new (grâce à un [n] dans l'expression-new), vous devez absolument utiliser les [] dans l'instruction delete. Cette syntaxe est nécessaire parce qu'il n'y a pas de différence syntaxique entre un pointeur sur quelque chose et un pointeur sur un tableau de quelque chose (C++ a hérité ça de C).

[ Haut | Bas | Rechercher ]


[16.11] Et si j'oublie les [] lorsque je delete un tableau alloué par un new T[n]?

All life comes to a catastrophic end.

Il incombe au programmeur, et non au compilateur, d'associer correctement les new T[n] à des delete[]. Si vous vous trompez, le compilateur ne génèrera pas de message d'erreur, ni à la compilation, ni à l'exécution. Vous risquez probablement de corrompre le tas. Voire pire. Il y a des chances pour que votre programme plante.

[ Haut | Bas | Rechercher ]


[16.12] Peut-on se passer des [] quand on fait un delete sur un tableau d'un type de base (char, int, etc)?

Non!

Certains programmeurs croient que les [] dans un delete[] p ont pour seul but de permettre au compilateur d'appeler les destructeurs pour tous les éléments du tableau. Ce raisonnement les conduit à penser qu'un tableau d'un type de base (comme char ou int) peut être détruit sans que l'on doive utiliser les []. Ainsi, ils partent du principe que le code qui suit est valide:

void userCode(int n)
{
char* p = new char[n];
// ...
delete p;
// <— ERREUR! Devrait être delete[] p !
}

Ce code n'est pas valide, et a des chances de provoquer une catastrophe à l'exécution. Il faut savoir que l'instruction delete p appelle la fonction operator delete(void*), alors que l'instruction delete[] p appelle elle la fonction operator delete[](void*). S'il est vrai que, par défaut, operator delete[](void*) appelle operator delete(void*), un utilisateur a tout à fait le droit de remplacer operator delete[](void*) par une autre fonction qui aurait un comportement différent (dans un tel cas, l'utilisateur remplacera probablement aussi le code de new de la fonction operator new[](size_t)). Au final, si le code de delete[] a été remplacé et n'est pas compatible avec le code de delete, et si vous avez appelé la mauvaise fonction (par exemple, si vous avez utilisé delete p au lieu de delete[] p), il y a grand risque de catastrophe à l'exécution.

[ Haut | Bas | Rechercher ]


[16.13] Après un p = new Fred[n], comment le compilateur sait-il que delete[] p doit détruire exactement n objets?

Réponse courte: c'est magique.

Réponse longue: le run-time C++ stocke le nombre d'objets, n, à un endroit où il peut le retrouver grâce uniquement au pointeur p. Il y a deux techniques classiques qui permettent de faire ça. Ces techniques, qui sont toutes deux utilisées dans des compilateurs payants, présentent chacune des inconvénients, et ni l'une ni l'autre n'est parfaite. Elles consistent à:

[ Haut | Bas | Rechercher ]


[16.14] Est-il légal (et moral) de faire un delete this dans une fonction membre?

En étant prudent, il n'y a pas de problème à ce qu'un objet se suicide (delete this).

Par "prudent", j'entends que:

  1. Vous devez être sûr à 100% que l'objet (this) a été alloué par new (et pas par new[], ni par un placement new ; de plus, cet objet ne doit pas être un objet local alloué sur la pile, ni un objet global, ni une donnée membre d'un autre objet; cet objet doit avoir été alloué par un new de base tout simple).
  2. Vous devez être sûr à 100% que la fonction membre invoquée est la dernière fonction membre qui sera invoquée pour this.
  3. Vous devez être sûr à 100% que la fin de votre fonction membre (la partie du code qui se trouve après le delete this) n'accède à aucune partie de this (elle ne doit ni appeler une autre fonction membre, ni lire ou modifier une donnée membre).
  4. Vous devez être sûr à 100% que personne ne manipule le pointeur this après le delete this. Ce pointeur de doit ni être lu, ni comparé à un autre pointeur, ni comparé à NULL, ni imprimé, ni casté, ni quoi que ce soit.

Les avertissements habituels s'appliquent au cas où votre pointeur this est un pointeur sur une classe de base et que vous n'avez pas de destructeur virtuel .

[ Haut | Bas | Rechercher ]


[16.15] Comment faire pour allouer un tableau à plusieurs dimensions en utilisant new?

De nombreuses techniques peuvent être utilisées, tout dépend du degré de flexibilité que vous souhaitez au niveau du dimensionnement du tableau. à un extrême, si vous connaissez toutes les dimensions du tableau à la compilation, vous pouvez simplement allouer statiquement un tableau à plusieurs dimensions (comme en C):

class Fred { /*...*/ };
void someFunction(Fred& fred);

void manipulateArray()
{
const unsigned nrows = 10;
// Le nombre de lignes est une constante de compilation
const unsigned ncols = 20;
// Le nombre de colonnes est une constante de compilation
Fred matrix[nrows][ncols];

for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Voilà comment on accède à l'élément (i,j):
someFunction( matrix[i][j] );

// Pas besoin de delete explicite, on peut faire simplement "return":
if (today == "Tuesday" && moon.isFull())
return;
// Traitement écourté les mardi de pleine lune (full moon = pleine lune)
}
}

// Pas besoin non plus de faire un delete explicite à la fin de la fonction
}

Plus fréquemment, la taille de la matrice n'est connue qu'à l'exécution mais on sait qu'il s'agit d'une matrice rectangulaire. Dans ce cas, il est nécessaire de se servir du tas ("heap", "freestore") tout en sachant que l'on peut allouer un seul bloc mémoire pour tous les éléments.

void manipulateArray(unsigned nrows, unsigned ncols)
{
Fred* matrix = new Fred[nrows * ncols];

// Puisqu'on a utilisé ci-dessus un pointeur "brut", il faut faire
// TRèS attention à ne pas oublier de faire un delete.
// C'est pourquoi on attrape toutes les exceptions:
try {

// Voilà comment on accède à l'élément (i,j):
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
someFunction( matrix[i*ncols + j] );
}
}

// Si vous voulez écourter le traitement les mardi de pleine lune, assurez-vous
// qu'il y a bien un delete avant TOUS les points de sortie de la fonction:
if (today == "Tuesday" && moon.isFull()) {
delete[] matrix;
return;
}

// ...

}
catch (...) {
// N'oubliez pas le delete dans le cas où une exception est lancée:
delete[] matrix;
throw;
// Relancer l'exception courante
}

// N'oubliez pas non plus le delete à la fin de la fonction:
delete[] matrix;
}

à l'autre extrême, il est des cas où vous n'avez pas la garantie que la matrice soit rectangulaire. Par exemple, si par chaque ligne peut avoir une longueur différente alors les lignes devront être allouées séparément. Dans la fonction ci-dessous, ncols[i] est le nombre de colonnes dans la ligne i, avec i variant de 0 à nrows-1 inclus.

void manipulateArray(unsigned nrows, unsigned ncols[])
{
Fred** matrix = new Fred*[nrows];
for (unsigned i = 0; i < nrows; ++i)
matrix[i] = new Fred[ ncols[i] ];

// Puisqu'on a utilisé ci-dessus un pointeur "brut", il faut faire
// TRèS attention à ne pas oublier de faire un delete.
// C'est pourquoi on attrape toutes les exceptions:
try {

// Voilà comment on accède à l'élément (i,j):
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols[i]; ++j) {
someFunction( matrix[i][j] );
}
}

// Si vous voulez écourter le traitement les mardi de pleine lune, assurez-vous
// qu'il y a bien un delete avant TOUS les points de sortie de la fonction:
if (today == "Tuesday" && moon.isFull()) {
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
return;
}

// ...

}
catch (...) {
// N'oubliez pas le delete dans le cas où une exception est lancée:
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
throw;
// Relancer l'exception courante
}

// N'oubliez pas non plus le delete à la fin de la fonction.
// Notez que la destruction se fait dans l'ordre inverse de l'allocation:
for (i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
}

Le fait de faire matrix[i-1] dans la boucle de destruction peut sembler bizarre. ça permet en fait d'éviter que i (qui est un unsigned) prenne une valeur très grande quand il vaut 0 et qu'il est décrémenté par la boucle for.

Pour finir, gardez à l'esprit que les pointeurs et les tableaux vous veulent du mal. Il vous sera en général très bénéfique d'encapsuler vos pointeurs dans une classe possédant une interface sûre et simple. La FAQ suivante donne un exemple de cette technique.

[ Haut | Bas | Rechercher ]


[16.16] Le code de la FAQ précédente est TELLLLLEMENT retors et source d'erreurs! Ne peut-on pas faire plus simple?

Si.

C'est parce qu'il utilise des pointeurs que le code de la FAQ précédente est si retors et si source d'erreurs, et nous savons que les pointeurs et les tableaux nous veulent du mal. La solution consiste à encapsuler les pointeurs dans une classe possédant une interface sûre et simple. On peut par exemple définir une classe Matrix représentant une matrice rectangulaire, ce qui va simplifier grandement le code que l'utilisateur aura a écrire si on le compare aucode de la FAQ précédente pour une matrice rectangulaire:

// Le code de la classe Matrix se trouve plus bas...
void someFunction(Fred& fred);

void manipulateArray(unsigned nrows, unsigned ncols)
{
Matrix matrix(nrows, ncols);
// Construit une Matrix nommée matrix

for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Voilà comment on accède à l'élément (i,j):
someFunction( matrix(i,j) );

// Pas besoin de delete explicite, on peut faire simplement "return":
if (today == "Tuesday" && moon.isFull())
return;
// Traitement écourté les mardi de pleine lune (full moon = pleine lune)
}
}

// Pas besoin non plus de faire un delete explicite à la fin de la fonction
}

Ce qu'il est important de noter, c'est qu'il n'est pas nécessaire d'écrire du code pour récupérer la mémoire. Il n'y a pas par exemple d'instruction delete dans le code ci-dessus, et pourtant il n'y aura pas de fuite mémoire (à la seule condition que le destructeur de la classe Matrix fasse correctement son travail).

Voici comment Matrix rend une telle chose possible:

class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// La méthode ci-dessus lance un objet BadSize si l'une des deux tailles vaut zéro
class BadSize { };

// Application de la Loi des Trois Soeurs:
~Matrix();
Matrix(const Matrix& m);
Matrix& operator= (const Matrix& m);

// Méthodes d'accès à l'élément (i,j):
Fred& operator() (unsigned i, unsigned j);
const Fred& operator() (unsigned i, unsigned j) const;
// Les deux méthodes ci-dessus lancent un objet BoundsViolation si i est trop grand ou si j est trop grand
class BoundsViolation { };

private:
Fred* data_;
unsigned nrows_, ncols_;
};

inline Fred& Matrix::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}

inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}

Matrix::Matrix(unsigned nrows, unsigned ncols)
: data_ (new Fred[nrows * ncols]),
nrows_ (nrows),
ncols_ (ncols)
{
if (nrows == 0 || ncols == 0)
throw BadSize();
}

Matrix::~Matrix()
{
delete[] data_;
}

Notez que cette classe Matrix accomplit deux choses: elle transfère la responsabilité du code retors de gestion mémoire depuis l'utilisateur (p.ex., main()) vers l'intérieur de la classe, et elle rend le programme globalement plus léger. Si on suppose que Matrix est un tant soit peu réutilisable, rendre la tâche plus facile aux utilisateurs de Matrix en déplaçant la complexité vers l'implémentation de la classe, revient à rendre la tâche plus facile au plus grand nombre au détriment de quelques-uns. Et si vous avez vu Star Trek 3, vous savez que le bien du plus grand nombre l'emporte sur le bien de quelques-uns, et à fortiori sur le bien d'un seul.

[ Haut | Bas | Rechercher ]


[16.17] La classe Matrix de la FAQ précédente est spécifique à Fred! Ne peut-on pas la rendre generique?

Si, il suffit d'utiliser les templates :

template<class T> // Voir la section sur les templates pour plus d'explications
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// La méthode ci-dessus lance un objet BadSize si l'une des deux tailles vaut zéro
class BadSize { };

// Application de la Loi des trois Soeurs:
~Matrix();
Matrix(const Matrix<T>& m);
Matrix<T>& operator= (const Matrix<T>& m);

// Méthodes d'accès à l'élément (i,j):
T& operator() (unsigned i, unsigned j);
const T& operator() (unsigned i, unsigned j) const;
// Les deux méthodes ci-dessus lancent un objet BoundsViolation si i est trop grand ou si j est trop grand
class BoundsViolation { };

private:
T* data_;
unsigned nrows_, ncols_;
};

template<class T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}

template<class T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}

template<class T>
inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
: data_ (new T[nrows * ncols]),
nrows_ (nrows),
ncols_ (ncols)
{
if (nrows == 0 || ncols == 0)
throw BadSize();
}

template<class T>
inline Matrix<T>::~Matrix()
{
delete[] data_;
}

Et voici un exemple d'utilisation de cette classe template :

#include "Fred.hpp" // Pour la définition de la classe Fred

void doSomethingWith(Fred& fred);

void sample(unsigned nrows, unsigned ncols)
{
Matrix<Fred> matrix(nrows, ncols);
// Construit une Matrix<Fred> nommée matrix

for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
doSomethingWith( matrix(i,j) );
}
}
}

[ Haut | Bas | Rechercher ]


[16.18] What's another way to build a Matrix template?

Use the standard vector template, and make a vector of vector.

The following uses a vector<vector<T> > (note the space between the two > symbols).

#include <vector>

template<class T>  // See section on templates for more
class Matrix {
public:
   Matrix(unsigned nrows, unsigned ncols);
   // Throws a BadSize object if either size is zero
   class BadSize { };

   // No need for any of The Big Three!

   // Access methods to get the (i,j) element:
   T&       operator() (unsigned i, unsigned j);
   const T& operator() (unsigned i, unsigned j) const;
   // These throw a BoundsViolation object if i or j is too big
   class BoundsViolation { };

private:
	std::vector<vector<T> > data_;
};

template<class T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
	if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
	return data_[row][col];
}

template<class T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
	if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
	return data_[row][col];
}

template<class T>
Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
   : data_ (nrows)
{
	if (nrows == 0 || ncols == 0)
		throw BadSize();
	for (unsigned i = 0; i < nrows; ++i)
		data_[i].resize(ncols);
}

[ Haut | Bas | Rechercher ]


[16.19] Y-a-t-il en C++ des tableaux dont la taille peut-être spécifiée à l'exécution?

Oui, au sens où la STL contient une classe template vector qui permet cela.

Non, au sens où on doit spécifier à la compilation la taille les tableaux C++ natifs.

Oui, au sens où, même pour les tableaux C++ natifs, la taille de la première dimension peut être spécifiée à l'exécution. Si seule la première dimension doit varier, il suffit de demander à new de construire un tableau de tableaux plutôt qu'un tableau de pointeurs sur des tableaux:

const unsigned ncols = 100; // ncols = nombre de colonnes dans le tableau

class Fred {
/*...*/ };

void manipulateArray(unsigned nrows)
// nrows = nombre de lignes dans le tableau
{
Fred (*matrix)[ncols] = new Fred[nrows][ncols];
// ...
delete[] matrix;
}

Il n'est pas possible de faire ça si une autre dimension que la première doit varier à l'exécution.

Mais SVP, n'utilisez les tableaux que si vous ne pouvez pas faire autrement. Les tableaux vous veulent du mal. Si possible, utilisez plutôt des objets.

[ Haut | Bas | Rechercher ]


[16.20] Comment faire un sorte que les objets d'une classe soient toujours créés par new plutôt qu'en tant qu'objets locaux ou globaux/static? UPDATED!
[Ajout récent (10/99): main() a maintenant un type de retour. Les FAQs modifiées récemment sont "chaînées". Cliquez ici pour aller à la suivante.]

Utilisez l'idiome du Constructeur Nommé.

Comme il se doit avec l'idiome du Constructeur Nommé, tous les constructeurs sont private: ou protected:, et on définit une ou plusieurs méthodes public static create() (les "constructeurs nommés"), une par constructeur. Les méthodes create() allouent les objets grâce à new. Et puisque les constructeurs ne sont pas public, il n'y a pas d'autre moyen de créer des objets de cette classe.

class Fred {
public:
// Les méthodes create() sont les "constructeurs nommés":
static Fred* create() { return new Fred(); }
static Fred* create(int i) { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
// ...

private:
// Les constructeurs sont eux private ou protected:
Fred();
Fred(int i);
Fred(const Fred& fred);
// ...
};

Les méthodes Fred::create() sont alors la seule façon de créer des objets Fred:

int main()
{
Fred* p = Fred::create(5);
// ...
delete p;
}

Si Fred doit être dérivée, assurez-vous bien que les constructeurs sont dans la section protected:.

Notez aussi que, si une classe Wilma doit avoir un objet Fred pour donnée membre, il est possible de rendre Wilma amie de Fred.. Mais on dévie alors du but initial, qui était d'obliger tous les objets Fred à être alloués par new.

[ Haut | Bas | Rechercher ]


[16.21] Comment faire du comptage de références simple?

Si tout ce dont vous avez besoin est de pouvoir avoir plusieurs pointeurs sur le même objet tout en étant assuré que cet objet soit détruit automagiquement quand le dernier pointeur disparaît, alors vous pouvez utilisez une technique similaire à la classe de "pointeur intelligent" ("smart pointer") suivante:

// Fred.h

class FredPtr;

class Fred {
public:
Fred() : count_(0)
/*...*/ { } // Tous les constructeurs initialisent count_ à 0 !
// ...
private:
friend FredPtr;
// Une classe amie
unsigned count_;
// count_ doit être initialisé à 0 par tous les constructeurs
// count_ représente le nombre d'objets FredPtr qui pointent sur this
};

class FredPtr {
public:
Fred* operator-> () { return p_; }
Fred& operator* () { return *p_; }
FredPtr(Fred* p) : p_(p) { ++p_->count_; }
// p ne doit pas valoir NULL
~FredPtr() { if (--p_->count_ == 0) delete p_; }
FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
FredPtr& operator= (const FredPtr& p)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
// (Cet ordre gère correctement le cas de l'auto-affectation)
++p.p_->count_;
if (--p_->count_ == 0) delete p_;
p_ = p.p_;
return *this;
}
private:
Fred* p_;
// p_ ne vaut jamais NULL
};

Vous pouvez évidemment utiliser une classe imbriquée, et transformer FredPtr en Fred::Ptr.

Notez qu'il est possible d'être moins restrictif sur le "p_ ne vaut jamais NULL" en faisant quelques tests supplémentaires dans le constructeur, dans le constructeur de copie, dans l'opérateur d'affectation et dans le destructeur. Si vous prenez cette voie, vous auriez aussi intérêt à faire le test p_ != NULL dans les opérateurs "*" et "->" (au pire faites un assert()). Par ailleurs, je serais tenté de vous déconseiller de fournir une méthode operator Fred*(), car une telle méthode pourrait donner à l'utilisateur accidentellement accès au Fred*.

L'une des contraintes implicites est qu'un objet FredPtr doit pointer uniquement sur des objets Fred qui ont été alloués par new. Si vous voulez le garantir, vous pouvez rendre private tous les constructeurs de Fred, et fournir pour chaque constructeur une méthode public (static) create() qui alloue l'objet Fred par un new et qui renvoie un FredPtr (pas un Fred*). Ainsi, la seule façon de créer un objet Fred est de demander un FredPtr ("Fred* p = new Fred()" devra être remplacé par "FredPtr p = Fred::create()"). Et personne ne peut corrompre accidentellement le mécanisme de comptage de références.

Si, par exemple, Fred avait les constructeurs Fred::Fred() et Fred::Fred(int i, int j), on changerait la classe Fred comme suit:

class Fred {
public:
static FredPtr create() { return new Fred(); }
static FredPtr create(int i, int j) { return new Fred(i,j); }
// ...
private:
Fred();
Fred(int i, int j);
// ...
};

Au final, vous disposez maintenant d'un moyen de faire du comptage de référence simple, qui vous permet de donner une "sémantique de pointeur" a un objet donné. Les utilisateurs de la classe Fred se servent d'objets FredPtr, lesquels se comportent plus ou moins comme des pointeurs Fred*. L'avantage est que les utilisateurs peuvent faire autant de copies qu'ils le souhaitent de leurs "pointeurs intelligents" FredPtr, tout en étant assurés que les objets Fred pointés vont être automagiquement détruits quand le dernier objet FredPtr object disparaîtra.

Si vous préférez fournir à vos utilisateurs une "sémantique de référence" plutôt qu'une "sémantique de pointeur," utilisez le comptage de références avec "copie sur écriture" ("copy on write").

[ Haut | Bas | Rechercher ]


[16.22] Comment faire du comptage de références avec copie-sur-écriture (copy-on-write)?

La FAQ précédente a présenté un mécanisme de comptage de références simple qui offre une sémantique de pointeur. Cette FAQ présente une approche qui offre aux utilisateurs une sémantique de référence.

L'idée de base est de laisser croire aux utilisateurs qu'ils font des copies d'objets Fred, alors qu'en réalité l'implémentation ne fait la copie que quand un utilisateur essaie de modifier l'objet Fred sous-jacent.

La classe Fred::Data stocke toutes les données qui seraient normalement placées dans la classe Fred. Fred::Data possède une donnée membre supplémentaire, count_, qui gère le compteur de références. La classe Fred elle-même devient finalement une "référence intelligente" qui pointe (en interne) sur un objet Fred::Data.

class Fred {
public:

Fred();
// Un constructeur par défaut
Fred(int i, int j);
// Un constructeur normal

Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();

void sampleInspectorMethod() const;
// Ne modifie pas l'objet (this)
void sampleMutatorMethod();
// Modifie l'objet (this)

// ...

private:

class Data {
public:
Data();
Data(int i, int j);
Data(const Data& d);

// étant donné que seule Fred peut accéder à l'objet Fred::Data, vous
// pouvez si vous le souhaitez rendre public les données de Fred::Data.
// Si ça ne vous plaît pas, mettez les données en private
// et faites de Fred une classe amie en utilisant friend Fred;
// ...

unsigned count_;
// count_ représente le nombre d'objets Fred qui pointent sur this
// count_ doit être initialisé à 1 par tous les constructeurs
// (on commence à 1 car l'objet Fred qui a créé ce Fred::data pointe dessus)
};

Data* data_;
};

Fred::Data::Data() : count_(1)
/*initialiser les autres données*/ { }
Fred::Data::Data(int i, int j) : count_(1)
/*initialiser les autres données*/ { }
Fred::Data::Data(const Data& d) : count_(1)
/*initialiser les autres données*/ { }

Fred::Fred() : data_(new Data()) { }
Fred::Fred(int i, int j) : data_(new Data(i, j)) { }

Fred::Fred(const Fred& f)
: data_(f.data_)
{
++ data_->count_;
}

Fred& Fred::operator= (const Fred& f)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
// (Cet ordre gère correctement l'auto-affectation)
++ f.data_->count_;
if (--data_->count_ == 0) delete data_;
data_ = f.data_;
return *this;
}

Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}

void Fred::sampleInspectorMethod() const
{
// Cette méthode garantit (elle est "const") de ne rien changer à *data_
// On utilise "data_->..." pour accéder aux données
}

void Fred::sampleMutatorMethod()
{
// Cette méthode pourrait modifier les données de *data_
// Il faut donc d'abord vérifier si this est le seul objet pointant sur *data_
if (data_->count_ > 1) {
Data* d = new Data(*data_);
// Appeler le copie-constructeur de Fred::Data
-- data_->count_;
data_ = d;
}
assert(data_->count_ == 1);

// à partir de là, la méthode accède normalement à "data_->..."
}

Si le constructeur par défaut de Fred est appelé assez fréquemment, il est possible d'éviter tous ces appels à new en partageant un objet Fred::Data, commun à tous les Freds construits par Fred::Fred(). Afin d'éviter les problèmes d'ordre d'initialisation des static, cet objet Fred::Data partagé est créé dans une fonction, "lors de la première utilisation". Voici les changements qu'il faudrait apporter au code ci-dessus pour utiliser cette deuxième approche (notez que le destructeur de l'objet partagé Fred::Data n'est jamais appelé. Si cela pose un problème et si vous voulez détruire l'objet, priez pour que vous n'ayez pas de problèmes dans l'ordre d'initialisation des static. Si c'est le cas, contentez-vous alors de la première approche):

class Fred {
public:
// ...
private:
// ...
static Data* defaultData();
};

Fred::Fred()
: data_(defaultData())
{
++ data_->count_;
}

Fred::Data* Fred::defaultData()
{
static Data* p = NULL;
if (p == NULL) {
p = new Data();
++ p->count_;
// Pour s'assurer qu'il n'atteigne jamais zéro
}
return p;
}

Note: si votre classe Fred doit être une classe de base, vous pouvez utiliser le comptage de référence pour une hiérarchie de classes.

[ Haut | Bas | Rechercher ]


[16.23] Comment faire du comptage de références avec copie-sur-écriture pour une hiérarchie de classes?

La FAQ précédente a présenté un mécanisme de comptage qui offre une sémantique de référence, mais ce pour une classe isolée plutôt que pour une hiérarchie de classes. Cette FAQ étend la technique précédente à une hiérarchie de classes. La différence de base est que Fred::Data est maintenant la racine d'une hiérarchie et possède des fonctions virtuelles. Notez que la classe Fred elle-même n'aura pas de fonctions virtuelles.

On utilise l'Idiome du Constructeur Virtuel pour copier les objets Fred::Data. Pour choisir la classe dérivée à créer, le code ci-dessous utilise l'Idiome du Constructeur Nommé, mais d'autres techniques sont possibles (un switch dans le constructeur, etc). Le code suppose qu'il existe deux classes dérivées: Der1 et Der2. Les méthodes des classes dérivées ne savent pas qu'il y a comptage de références.

class Fred {
public:

static Fred create1(String s, int i);
static Fred create2(float x, float y);

Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();

void sampleInspectorMethod() const;
// Ne modifie pas l'objet (this)
void sampleMutatorMethod();
// Modifie l'objet (this)

// ...

private:

class Data {
public:
Data() : count_(1) { }
Data(const Data& d) : count_(1) { }
// Ne PAS copier le membre 'count_'!
Data& operator= (const Data&) { return *this; }
// Ne PAS copier le membre 'count_'!
virtual ~Data() { assert(count_ == 0); }
// Un destructeur virtuel
virtual Data* clone() const = 0;
// Un constructeur virtuel
virtual void sampleInspectorMethod() const = 0;
// Une fonction virtuelle pure
virtual void sampleMutatorMethod() = 0;
private:
unsigned count_;
// Pas besoin de rendre count_ protected
friend Fred;
// Donner à Fred accès à count_
};

class Der1 : public Data {
public:
Der1(String s, int i);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};

class Der2 : public Data {
public:
Der2(float x, float y);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};

Fred(Data* data);
// Crée une "référence intelligente" Fred qui possède *data
// Ce constructeur est privé afin de forcer l'utilisation d'une méthode createXXX()
// Contrainte: data ne doit pas valoir NULL

Data* data_;
// Invariant: data_ ne vaut jamais NULL
};

Fred::Fred(Data* data) : data_(data) { assert(data != NULL); }

Fred Fred::create1(String s, int i) { return Fred(new Der1(s, i)); }
Fred Fred::create2(float x, float y) { return Fred(new Der2(x, y)); }

Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }

Fred::Fred(const Fred& f)
: data_(f.data_)
{
++ data_->count_;
}

Fred& Fred::operator= (const Fred& f)
{
// NE CHANGEZ PAS L'ORDRE DE CES INSTRUCTIONS!
// (Cet ordre gère correctement l'auto-affectation)
++ f.data_->count_;
if (--data_->count_ == 0) delete data_;
data_ = f.data_;
return *this;
}

Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}

void Fred::sampleInspectorMethod() const
{
// Cette méthode garantit (elle est "const") de ne rien changer à *data_
// On se contente donc simplement de "passer la main" à *data_:
data_->sampleInspectorMethod();
}

void Fred::sampleMutatorMethod()
{
// Cette méthode pourrait modifier les données de *data_
// Il faut donc d'abord vérifier si this est le seul objet pointant sur *data_
if (data_->count_ > 1) {
Data* d = data_->clone();
// L'idiome du Constructeur Virtuel
-- data_->count_;
data_ = d;
}
assert(data_->count_ == 1);

// On "passe ensuite la main" à *data_:
data_->sampleInspectorMethod();
}

Vous devrez bien sûr implémenter, de la façon qui convient, les constructeurs et les méthodes sampleXXX de Fred::Der1 et de Fred::Der2.

[ Haut | Bas | Rechercher ]


[16.24] Can you absolutely prevent people from subverting the reference counting mechanism, and if so, should you?

No, and (normally) no.

There are two basic approaches to subverting the reference counting mechanism:

  1. The scheme could be subverted if someone got a Fred* (rather than being forced to use a FredPtr). Someone could get a Fred* if class FredPtr has an operator*() that returns a Fred&: FredPtr p = Fred::create(); Fred* p2 = &*p;. Yes it's bizarre and unexpected, but it could happen. This hole could be closed in two ways: overload Fred::operator&() so it returns a FredPtr, or change the return type of FredPtr::operator*() so it returns a FredRef (FredRef would be a class that simulates a reference; it would need to have all the methods that Fred has, and it would need to forward all those method calls to the underlying Fred object; there might be a performance penalty for this second choice depending on how good the compiler is at inlining methods). Another way to fix this is to eliminate FredPtr::operator*() — and lose the corresponding ability to get and use a Fred&. But even if you did all this, someone could still generate a Fred* by explicitly calling operator->(): FredPtr p = Fred::create(); Fred* p2 = p.operator->();.
  2. The scheme could be subverted if someone had a leak and/or dangling pointer to a FredPtr Basically what we're saying here is that Fred is now safe, but we somehow want to prevent people from doing stupid things with FredPtr objects. (And if we could solve that via FredPtrPtr objects, we'd have the same problem again with them). One hole here is if someone created a FredPtr using new, then allowed the FredPtr to leak (worst case this is a leak, which is bad but is usually a little better than a dangling pointer). This hole could be plugged by declaring FredPtr::operator new() as private, thus preventing someone from saying new FredPtr(). Another hole here is if someone creates a local FredPtr object, then takes the address of that FredPtr and passed around the FredPtr*. If that FredPtr* lived longer than the FredPtr, you could have a dangling pointer — shudder. This hole could be plugged by preventing people from taking the address of a FredPtr (by overloading FredPtr::operator&() as private), with the corresponding loss of functionality. But even if you did all that, they could still create a FredPtr& which is almost as dangerous as a FredPtr*, simply by doing this: FredPtr p; ... FredPtr& q = p; (or by passing the FredPtr& to someone else).

And even if we closed all those holes, C++ has those wonderful pieces of syntax called pointer casts. Using a pointer cast or two, a sufficiently motivated programmer can normally create a hole that's big enough to drive a proverbial truck through. (By the way, pointer casts are evil.)

So the lessons here seems to be: (a) you can't prevent espionage no matter how hard you try, and (b) you can easily prevent mistakes.

So I recommend settling for the "low hanging fruit": use the easy-to-build and easy-to-use mechanisms that prevent mistakes, and don't bother trying to prevent espionage. You won't succeed, and even if you do, it'll (probably) cost you more than it's worth.

So if we can't use the C++ language itself to prevent espionage, are there other ways to do it? Yes. I personally use old fashioned code reviews for that. And since the espionage techniques usually involve some bizarre syntax and/or use of pointer-casts and unions, you can use a tool to point out most of the "hot spots."

[ Haut | Bas | Rechercher ]


[16.25]Can I use a garbage collector in C++?

Yes.

Compared with the "smart pointer" techniques (see [16.21], the two kinds of garbage collector techniques (see [16.26]) are:

[ Haut | Bas | Rechercher ]


[16.26]What are the two kinds of garbage collectors for C++?

In general, there seem to be two flavors of garbage collectors for C++:

  1. Conservative garbage collectors. These know little or nothing about the layout of the stack or of C++ objects, and simply look for bit patterns that appear to be pointers. In practice they seem to work with both C and C++ code, particularly when the average object size is small. Here are some examples, in alphabetical order:

  2. Hybrid garbage collectors. These usually scan the stack conservatively, but require the programmer to supply layout information for heap objects. This requires more work on the programmer's part, but may result in improved performance. Here are some examples, in alphabetical order:

Since garbage collectors for C++ are normally conservative, they can sometimes leak if a bit pattern "looks like" it might be a pointer to an otherwise unused block. Also they sometimes get confused when pointers to a block actually point outside the block's extent (which is illegal, but some programmers simply must push the envelope; sigh) and (rarely) when a pointer is hidden by a compiler optimization. In practice these problems are not usually serious, however providing the collector with hints about the layout of the objects can sometimes ameliorate these issues.

[ Haut | Bas | Rechercher ]


[16.27]Where can I get more info on garbage collectors for C++?
For more information, see the Garbage Collector FAQ.

[ Haut | Bas | Rechercher ]


E-mail Marshall Cline Ecrire à l'auteur, au traducteur, ou en savoir plus sur la traduction.
C++ FAQ Lite fr | Table des matières | Index | A propos de l'auteur | © | Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:17 PDT 2003