Identifier l'adresse précise d'un objet en mémoire

J'ai pris l'idée pour cet article d'une manoeuvre sympathique de Scott Meyers dans son livre More Effective C++. Pour comprendre cet article, il est préférable d'avoir au préalable lu et compris celui sur les conversions explicites de types ISO. Il faut aussi comprendre la mécanique du polymorphisme.

Il peut arriver, lors de manipulations de bas niveau, qu'un programme souhaite connaître l'adresse exacte en mémoire d'un objet. C'est parfois chose simple, comme dans le cas proposé ci-dessous :

#include <string>
#include <iostream>
int main() {
   using namespace std;
   string s = "J'aime mon prof!";
   cout << "La chaîne s, de contenu \"" << s << "\" est à l'adresse "
        << &s << endl;
}

Cet exemple est simple parce que std::string est un type valeur, sans parent ni héritage multiple ou virtuel. Dans un cas plus complexe, la situation est beaucoup moins simple qu'il n'y paraît.

Par exemple, prenons la classe Bonhomme proposée à droite. Dans cet exemple, tout Bonhomme est aussi un Acteur et un Affichable. Cela implique que dans un Bonhomme, il y a un Acteur et il y a aussi un Affichable.

Il est donc possible de déposer un pointeur de Bonhomme dans un pointeur de Affichable, tout comme il est possible de déposer un pointeur de Bonhomme dans un pointeur de Acteur. Le programme principal le montre d'ailleurs clairement. Il est aussi possible de prendre l'adresse d'un Bonhomme directement, en ayant recours à l'opérateur & unaire (présumant que cet opérateur n'ait pas été surchargé pour Bonhomme... et nous ne ferons pas une telle horreur, n'est-ce pas?).

Cela dit, le code proposé à droite plante sauvagement à l'exécution. En déposant les trois adresses dans des void * (des adresses pures) pour faciliter leur comparaison, ce qui change le type de ces adresses sans changer leur valeur, il devient possible de les comparer entre elles... or, l'assertion dynamique (assert()) fera planter l'exécution du programme. Les pointeurs p0 (adresse de b en tant que Bonhomme), p1 (adresse de b en tant qu'Acteur) et p2 (adresse de b en tant qu'Affichable) ne sont pas égales!

Cela s'explique d'ailleurs aisément : dans un Acteur, il n'y a pas d'Affichable et dans un Affichable, il n'y a pas d'Acteur. Déposer l'adresse de b dans un Acteur * implique un léger déplacement en mémoire pour que le pointeur d'Acteur pointe à l'endroit, dans le Bonhomme, où se situe l'Acteur en lui.

Les deux adresses seront identiques seulement si la partie Acteur se situe tout au début d'un Bonhomme (ce qui peut être, ou non, le cas). évidemment, Bonhomme ayant ici deux parents, il est peu probable (même si ça peut arriver dans des cas pointus) que les deux se situent précisément au même endroit.

struct Affichable {
   // différents trucs
   virtual void afficher() const = 0;
   virtual ~Affichable() = default;
};
struct Acteur {
   // différents trucs
   virtual void agir() = 0;
   virtual ~Acteur() = default;
};
class Bonhomme
   : public Affichable, public Acteur {
   // différents trucs
   void agir() {
      // ...
   }
   void afficher() const {
      // ...
   }
};
template <class T>
   void* adresse_de(T *p) {
      return static_cast<void *>(p);
      // ou, pour le même effet, return (void*)p;
   }
#include <cassert>
int main() {
   Bonhomme b;
   Acteur *pActeur = &b;
   Affichable *pAffichable = &b;
   auto p0 = adresse_de(&b);
   auto p1 = adresse_de(pActeur);
   auto p2 = adresse_de(pAffichable);
   assert(p0 == p1 && p0 == p2); // BOUM!
}

La situation serait d'ailleurs la même (mis à part quelques cas pathologiques) avec au moins deux niveaux d'héritage simple, et n'est pas propre à l'héritage multiple. Elle surviendrait aussi sans que les classes impliquées n'aient de méthodes polymorphiques (nous le verrons d'ailleurs sous peu, le polymorphisme fait ici partie de la solution, pas du problème). Cela dit, si nous avons une instance de Bonhomme nommée b, alors son adresse en mémoire est clairement &b mais, de manière générale, avec un Acteur * par exemple, comment savoir où se trouve précisément en mémoire l'instance dont fait partie ce vers quoi mène ce pointeur?

La clé est de choisir une conversion explicite de types plus intelligente. Le recours à l'opérateur static_cast dans l'expression static_cast<void *> (ou, ce qui revient au même ici, le recours à une conversion explicite du langage C dans l'expression (void *)p) est, comme toutes les conversions explicites de types, un mensonge, mais ce mensonge est trop simple pour nos fins : il signifie simplement « Je sais que ce n'est pas tout à fait vrai, ami compilateur, mais fais semblant s'il te plaît que ce pointeur est une simple adresse brute ». Le glissement d'adresses requis pour que le pointeur mène vers le tout début de l'objet pointé ne sera pas réalisé; un static_cast n'est qu'un engagement de la part de la programmeuse ou du programmeur que le mensonge n'entraînera pas de consquences néfastes.

En retour, il existe en C++ un opérateur de conversion explicite de types ISO, plus lent mais aussi plus sophistiqué, qui est en mesure de naviguer une hiérarchie de classes pour retrouver l'adresse d'une partie d'un objet, et cet opérateur est dynamic_cast. Il n'est cependant applicable qu'aux pointeurs dans lesquels on trouve une table de méthodes virtuelles (donc à travers un pointeur sur un type polymorphique), ce qui explique la remarque plus haut selon laquelle le polymorphisme fait ici partie de la solution, pas du problème. De toute manière, la majorité des cas raisonnables pour lesquels on pourrait vouloir retrouver une partie non évidente d'un objet impliquent de percevoir l'objet à travers plusieurs lorgnettes, plusieurs points de vue mutuellement abstraits, et cela signifie très fréquemment manipuler des abstractions spécialisables sur un objet – des abstractions polymorphiques.

Une version opérationnelle et correcte de l'exemple de code proposé plus haut est celui que vous pouvez voir à droite.

Remarquez le léger changement apporté à la fonction adresse_de() : elle utilise maintenant un dynamic_cast<void *> pour réaliser la conversion de T * (type du pointeur passé en paramètre) en void * (adresse pure). L'opérateur est légal parce que le type source, T *, est dans chaque cas polymorphique – chacun de nos trois types expose au moins une méthode virtuelle.

Il peut vous paraître étrange que dynamic_cast, un opérateur destiné à convertir entre eux des pointeurs ou des références sur des objets, puisse réaliser une conversion en adresse pure. Il peut aussi vous sembler étrange (pensez-y bien!) que la conversion en adresse pure donne l'adresse exacte de l'objet réellement pointé en mémoire. Quelle est la relation hiérarchique entre void * et les diverses classes impliquées qui permet (et rend pleinement valide) cette conversion?

La raison, la voici : le type représentant une adresse pure, void *, est l'abstraction la plus élevée du langage C++. En fait, void * est en quelque sorte à C++ ce que Object est à Java et ce que System.Type est aux langages .NET. Ce sont des idées différentes mais, au sens qui nous intéresse ici, apparentées.

Dans l'exemple à droite, Acteur est plus abstrait que Bonhomme puisqu'un Bonhomme est aussi un Acteur mais que le contraire est faux. Le même raisonnememnt s'applique aux classes Affichable et Bonhomme. Il n'y a pas de relation d'abstraction entre Acteur et Affichable; les deux classes n'ont ici de lien qu'à travers un enfant commun, ce qui permettrait par exemple d'obtenir un Acteur * à travers un Affichable * dans le cas où les deux pointeurs mènent en fait vers un Bonhomme mais pas nécessairement dans d'autres circonstances – il peut y avoir des enfants de Affichable qui ne sont pas aussi des enfants de Acteur.

struct Affichable {
   // différents trucs
   virtual void afficher() const = 0;
   virtual ~affichable() = default;
};
struct Acteur {
   // différents trucs
   virtual void agir() = 0;
   virtual ~acteur() = default;
};
class Bonhomme
   : public Affichable, public Acteur {
   // différents trucs
   void agir() {
      // ...
   }
   void afficher() const {
      // ...
   }
};
template <class T>
   void * adresse_de(T *p) {
      return dynamic_cast<void *>(p);
   }
#include <cassert>
int main() {
   Bonhomme b;
   Acteur *pActeur = &b;
   Affichable *pAffichable = &b;
   auto p0 = adresse_de(&b);
   auto p1 = adresse_de(pActeur);
   auto p2 = adresse_de(pAffichable);
   assert(p0 == p1 && p0 == p2); // OK!
}

Quand on élève le niveau d'abstraction à son maximum, il ne reste que la plus pure de toute : le concept d'être quelque part, le concept de lieu, d'adresse pure. L'opérateur peut donc naviguer jusqu'à la classe terminale derrière le pointeur (ici, dans chaque cas, vers le Bonhomme nommé b) et obtenir son adresse. Ainsi, convertir un pointeur polymorphique en adresse pure à l'aide d'un dynamic_cast permet de retrouver le lieu réel en mémoire de l'objet effectivement pointé. évidemment, pas question de faire le chemin inverse à partir de l'adresse pure, celle-ci n'ayant auxcune connotation sémantique (et un void * n'exposant, par définition, pas de méthodes virtuelles). Une fois l'adresse pure obtenue, à vous de savoir ce que vous souhaitez en faire...


Valid XHTML 1.0 Transitional

CSS Valide !