Pointeur sur (peu importe quoi)

J'ai eu l'idée de ce petit exercice en examinant le texte http://www.codeproject.com/KB/cpp/safeGenericPointer.aspx proposé par Francis Xavier sur le Code Project. Je n'ai pas regardé ses sources alors j'ai fait un petit truc dans la même lignée, pour m'amuser, en me disant que ça pourrait intéresser mes étudiant(e)s. L'idée originale, donc, est de lui, pas de moi, mais l'ébauche d'implémentation décrite ici est la mienne. Notez que ceci n'est pas aussi sophistiqué que la technique décrite dans l'article sur l'effacement de types, mais ça peut donner des idées.

Imaginons un programme C++ comme celui décrit à droite. Remarquez le type any_ptr et l'usage qui en est fait :

  • On y dépose des pointeurs de différents types (un int*, puis un string*)
  • On peut tenter de traiter un any_ptr comme un pointeur d'un autre type (implicitement, par une affectation ou une construction où l'any_ptr est traité comme un rvalue, ou explicitement, par un static_cast)
  • Ces tentatives donnent un pointeur nul (ou, si vous préférez procéder ainsi, pourraient lever une exception) quand la conversion de ramène pas le type pointé à ce moment par l'any_ptr, ou un pointeur correct dans le cas où la conversion résulte bel et bien en un pointeur du type d'origine

Nous souhaitons un affichage comme celui ci-dessous lors de l'exécution du programme exemple proposé ici :

3
-=-=-=-=-=-=-
j'aime mon prof
-=-=-=-=-=-=-
p pointe sur une string de longueur 15
-=-=-=-=-=-=-
Appuyez sur une touche pour continuer...

Comment pourrions-nous implémenter simplement le type any_ptr?

Vous remarquerez qu'un any_ptr dans cet exemple serait un type de pointeur intelligent implémentant une sémantique de référence : il mène vers un pointeur mais n'en est pas responsable.

Nous pourrions supporter d'autres pointeurs intelligents dans any_ptr, plutôt que nous limiter à des pointeurs bruts, mais cela compliquerait singulièrement le code. Un exercice pour gens courageux!

Notez aussi que nous ne mènerons pas l'entreprise jusqu'au point où nous supporterions les conversions correctes au sens des qualifications const et volatile (ce serait amusant, mais beaucoup plus complexe – vous pouvez vous amuser à le faire si le coeur vous en dit).

#include "any_ptr.h"
#include <iostream>
#include <string>
int main() {
   using namespace std;
   int i = 3;
   any_ptr p = &i;
   int *q = p;
   if (q) cout << *q << endl;
   string *s = p;
   if (s) cout << *s << endl;
   cout << "-=-=-=-=-=-=-" << endl;
   string txt = "j'aime mon prof";
   p = &txt;
   q = p;
   if (q) cout << *q << endl;
   s = p;
   if (s) cout << *s << endl;
   cout << "-=-=-=-=-=-=-" << endl;
   if(static_cast<int*>(p))
      cout << "p pointe sur un int de valeur "
           << *static_cast<int*>(p) << endl;
   if(static_cast<string*>(p))
      cout << "p pointe sur une string de longueur "
           << static_cast<string*>(p)->size() << endl;
   cout << "-=-=-=-=-=-=-" << endl;
   // p = i; // illégal
}

Le type any_ptr (ébauche)

Le type any_ptr (ou du moins l'ébauche simplifiée que j'ai pris le temps de rédiger) se décline comme proposé à droite :

  • L'entreposage à l'interne est réalisé avec un pointeur abstrait (un void*). Notez que ceci ne suffirait pas nécessairement si nous souhaitions assurer les conversions sécuritaires du point de vue des qualifications const et volatile
  • Le descriptif du type pointé est mémorisé par un type_info*, donc cette approche repose en partie sur les mécanismes de Run-Time Type Information, ou RTTI. Il faut donc plus précisément inclure <typeinfo>, utiliser l'opérateur typeid(), et s'assurer que le code est compilé avec support pour RTTI puisque ce support, du fait qu'il a un impact sur la taille des programmes, est habituellement optionnel
  • Pour un any_ptr par défaut, nous supposerons que le pointé est un void* nul, pour faire en sorte que les conversions vers des types de pointeurs autres que void* soient illégaux
  • Pour un any_ptr contenant un type T quelconque, nous capturons le typeid(T) localement pour référence ultérieure et nous conservons l'équivalent void* du T en question dans un attribut d'instance. Une validation statique est réalisée pour éviter que T ne soit pas un type de pointeur
  • La conversion implicite en T pour un type T donné réalise aussi une validation statique pour éviter que T ne soit pas un type de pointeur, puis valide la conformité de typeid(T) et du type pointé par l'any_ptr en question. Si les types sont identiques, alors une conversion statique est réalisée; si les types différent, un pointeur nul est retourné
#include <typeinfo>
#include <type_traits>
class any_ptr {
   const std::type_info *inf;
   void *ptr;
   template <class T>
      void *capture(T p) {
         static_assert(std::is_pointer<T>::value, "type pointeur requis");
         inf = &typeid(T);
         return p;
      }
public:
   any_ptr() : ptr{capture(static_cast<void *>(nullptr))} {
   }
   template <class T>
      any_ptr(T p) : ptr{capture(p)} {
      }
   template <class T>
      any_ptr &operator=(T p) {
         ptr = capture(p);
         return *this;
      }
   template <class T>
      operator T() const {
         static_assert(std::is_pointer<T>::value, "type pointeur requis");
         return typeid(T) == *inf ? static_cast<T>(ptr) : nullptr;
      }
};

Petit détail : on ne peut utiliser capture() telle qu'elle est écrite sur nullptr et son type, ce qui explique que le pointeur d'un any_ptr par défaut soit non pas nullptr mais bien un void* de valeur 0.

Et voilà!

Si vous utilisez un compilateur pré-C++ 11...

La classe any_ptr décrite plus bas est une classe légale en C++ 11. Cependant, il est possible d'ajouter quelques outils écrits à la main pour que celle-ci compile sur un compilateur C++ 03 si cela s'avère nécessaire pour vous.

Les outils à ajouter reposent prncipalement sur des techniques de métaprogrammation et permettent d'assurer une forme de résilience minimale dans l'implémentation de la classe any_ptr.

Ils sont décrits à droite (vous pouvez donc les utiliser si nécessaire) :

  • Le trait is_pointer, tel que is_pointer<T>::value s'avèrera seulement si T est un type de pointeur. Sa définition par exhaustion est classique. En C++ 11, on inclura simplement <type_traits> et on utilisera std::is_pointer<T>::value pour en arriver au même résultat
  • L'assertion statique, permettra de valider des a priori connus à la compilation (incluant le fait que le code client doive utiliser any_ptr seulement pour entreposer des pointeurs). Depuis C++ 11, cette technique est caduque car le mot static_assert est devenu un mot clé du langage, et le support langagier de ce concept est bien plus élégant que ne l'étaient les manoeuvres « maison » que nous utilisions traditionnellement

L'article sur la métaprogrammation donne plus de détails sur chacune de ces techniques.

template <class T, T val>
   struct integral_constant {
      using type = T;
      static const T value = val; // idéalement constexpr
   };
struct true_type : integral_constant<bool, true> {
};
struct false_type : integral_constant<bool, false> {
};
template <class>
   struct is_pointer : false_type {
   };
template <class T>
   struct is_pointer<T*> : true_type {
   };
template <class T>
   struct is_pointer<const T*> : true_type {
   };
template <class T>
   struct is_pointer<volatile T*> : true_type {
   };
template <class T>
   struct is_pointer<const volatile T*> : true_type {
   };
template <bool>
   struct static_assert;
template <>
   struct static_assert<true> {
   };
template <class T>
   void unused(const T &) {
   }

Valid XHTML 1.0 Transitional

CSS Valide !