Etude du C++ Technical Report 1 - smart pointers | 0 vote(s)
Par Emmanuel Deloget, jeudi 8 mars 2007 à 12:00 :: C++ :: permalien #55
Il doit être noté qu'à l'heure actuelle, il est difficile de tester les fonctionnalités du TR1 - pour la bonne raison que certains compilateurs ne supportent pas cette extension. Il est toutefois possible d'acheter une licence de cette librairie chez Dinkumware Ltd., ou tout simplement d'utiliser boost (qui propose une certain nombre de classes du TR1, mais qui ne l'implémente pas complètement). Les dernières versions de GCC implémentent une partie du TR1. Visual C++ (même la version .NET 2005) ne propose les nouvelles interfaces définies. Quant aux compilateurs Borland (en particulier les versions Turbo Explorer gratuites), elles sont accompagnées de la suite Dinkumware.
Ah oui, dernière chose : inutile d'essayer de compiler ce code avec Visual C++ 6. Lorsque je parle de compilateurs, je parle bien évidemment de compilateurs C++, et VC6 n'en est pas un.
L'interface shared_ptr
Notre étude s'étoffe ! Après les type_traits, et en attendant (toujours) une ré-étude de reference_wrapper[1], nous étudions aujourd'hui les nouveaux types de pointeurs intelligents proposés par le TR1 : l'interface shared_ptr.
Note : Vu la taille du sujet étudié, ce billet est exceptionnellement scindé en deux parties distinctes. La seconde partie sera publiée dans un futur proche.
Motivations
Il existe plusieurs raisons quand à l'existence d'une interface shared_ptr dans le TR1. La première est que le C++ a désespérément besoin d'un tel outil - un type de smart pointer correctement défini, et répondant aux exigences des différents conteneurs de la librairie standard. En effet, le seul smart pointer disponible à l'heure actuelle est std::auto_ptr<>. Or la sémantique de ce type n'est pas compatible avec la sémantique des conteneurs. Ainsi, au lieu d'être CopyConstructible et Assignable, il utilise une sémantique de transfert de propriété.
std::auto_ptr<A> pa_1(new A); std::auto_ptr<A> pa_2(pa_1); assert(pa_1.get() == pa_2.get()); // ASSERTION FAILURE
De fait, cette sémantique parait illogique dans le cadre d'une utilisation normale, où l'on pourrait s'attendre à ce que ce code ne génère pas d'erreur d'assertion.
La seconde raison est que le principe d'un objet encapsulant un compteur de référence est souhaitable, ainsi que le montre l'important nombre d'implémentation différentes (et souvent peu fonctionnelles) des différents smart pointers. Il était donc logique que le comité de normalisation du C++ se penche sur le problème.
Comme pour d'autres types inclus dans le TR1, ils ne sont pas partis de rien. Ils ont en effet utilisé la librairie Boost.SmartPtr, qui a longtemps été une référence en la matière.
Présentation
L'interface shared_ptr se compose de plusieurs classes, ayant chacune un but différent[2]. Elles sont définies dans le fichier header <memory>.
La première (la plus simple) est la classe tr1::bad_weak_ptr, qui dérive de std::exception.
class bad_weak_ptr: public std::exception
{
public:
bad_weak_ptr();
};
Cette exception est générée lorsqu'on souhaite construire une instance de tr1::shared_ptr<> à l'aide d'un tr1::weak_ptr invalide.
La seconde est tr1::weak_ptr<>, qui encapsule un pointeur et son compteur de référence.
template<class T> class weak_ptr
{
public:
typedef T element_type;
// constructors
weak_ptr();
template<class Y> weak_ptr(shared_ptr<Y> const& r);
weak_ptr(weak_ptr const& r);
template<class Y> weak_ptr(weak_ptr<Y> const& r);
// destructor
~weak_ptr();
// assignment
weak_ptr& operator=(weak_ptr const& r);
template<class Y> weak_ptr&
operator=(weak_ptr<Y> const& r);
template<class Y> weak_ptr&
operator=(shared_ptr<Y> const& r);
// modifiers
void swap(weak_ptr& r);
void reset();
// observers
long use_count() const;
bool expired() const;
shared_ptr<T> lock() const;
};
// comparison
template<class T, class U>
bool operator<(weak_ptr<T> const& a,
weak_ptr<U> const& b);
// specialized algorithms
template<class T>
void swap(weak_ptr<T>& a, weak_ptr<T>& b);
Lorsque l'instance tr1::weak_ptr<> est détruite, la mémoire associée au pointeur encapsulé n'est pas libérée. On peut construire un tr1::weak_ptr<> à partir d'un autre tr1::weak_ptr<> ou d'un tr1::shared_ptr<>.
Vient ensuite la classe tr1::shared_ptr<> et les fonctions libres qui lui sont associées, définies ainsi :
template<class T> class shared_ptr
{
public:
typedef T element_type;
// [2.2.3.1] constructors
shared_ptr();
template<class Y>
explicit shared_ptr(Y* p);
template<class Y, class D>
shared_ptr(Y* p, D d);
shared_ptr(shared_ptr const& r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
template<class Y>
explicit shared_ptr(weak_ptr<Y> const& r);
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
// [2.2.3.2] destructor
~shared_ptr();
// [2.2.3.3] assignment
shared_ptr& operator=(shared_ptr const& r);
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);
// [2.2.3.4] modifiers
void swap(shared_ptr& r);
void reset();
template<class Y>
void reset(Y* p);
template<class Y, class D>
void reset(Y* p, D d);
// [2.2.3.5] observers
T* get() const;
T& operator*() const;
T* operator->() const;
long use_count() const;
bool unique() const;
operator unspecified-bool-type () const;
};
// [2.2.3.6] shared_ptr comparisons
template<class T, class U>
bool operator==(shared_ptr<T> const& a,
shared_ptr<U> const& b);
template<class T, class U>
bool operator!=(shared_ptr<T> const& a,
shared_ptr<U> const& b);
template<class T, class U>
bool operator<(shared_ptr<T> const& a,
shared_ptr<U> const& b);
// [2.2.3.7] shared_ptr I/O
template<class E, class T, class Y>
basic_ostream<E, T>& operator<< (basic_ostream<E, T>& os,
shared_ptr<Y> const& p);
// [2.2.3.8] shared_ptr specialized algorithms
template<class T>
void swap(shared_ptr<T>& a, shared_ptr<T>& b);
// [2.2.3.9] shared_ptr casts
template<class T, class U>
shared_ptr<T> static_pointer_cast(shared_ptr<U> const& r);
template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const& r);
template<class T, class U>
shared_ptr<T> const_pointer_cast(shared_ptr<U> const& r);
// [2.2.3.10] shared_ptr get_deleter
template<class D, class T>
D* get_deleter(shared_ptr<T> const& p);
Lorsque l'instance tr1::shared_ptr<> est détruite, la mémoire associée au pointeur encapsulé est libérée si this->use_count() == 0. On peut construire un tr1::shared_ptr<> à partir d'un tr1::weak_ptr<> ou d'un autre tr1::shared_ptr<>. Dans ces cas, this->use_count() est incrémenté.
La dernière classe est une classe d'aide: tr1::enable_shared_from_this<>, qui permet lorsqu'on en dérive une classe de simplifier la création de nouveaux objets de types tr1::shared_ptr<>. On verra par la suite comment l'utiliser.
template<class T> class enable_shared_from_this
{
protected:
enable_shared_from_this();
enable_shared_from_this(enable_shared_from_this const&);
enable_shared_from_this&
operator=(enable_shared_from_this const&);
~enable_shared_from_this();
public:
shared_ptr<T> shared_from_this();
shared_ptr<T const> shared_from_this() const;
};
Quelques notes avant de continuer :
- Puisque seul le
tr1::shared_ptr<>peut désallouer la mémoire associée au pointeur encapsulé, il est évident que les objetstr1::weak_ptr<>ont une durée de vie plus courte que lestr1::shared_ptr<>. Après la libération du derniertr1::shared_ptr<>controlant une ressource, la ressource est détruite, et tous lestr1::weak_ptr<>pointant sur cette ressource sont expirés (wp.expired() == true). - Après la construction d'un objet
tr1::shared_ptr<>à partir d'untr1::shared_ptr<>ou d'untr1::weak_ptr<>, le TR1 précise quenewObject.use_count() == source.use_count()- non seulement le nombre d'utilisations est incrémenté, mais il est aussi synchronisé. La création d'untr1::weak_ptr<>à partir de l'un de ces deux types n'augmente pas la valeuruse_count(). - bien évidemment, il est dangereux de construire deux
tr1::shared_ptr<>Ã partir d'un seul pointeur:
A *a = new A;
{
tr1::shared_ptr<A> spa_1(a);
tr1::shared_ptr<A> spa_2(a);
} // destroyes 'a' twice
- Assigner une nouvelle valeur à un
tr1::shared_ptr<>libère la valeur précédente (avec tout ce que celà implique sithis->use_count() == 0). - le TR1 précise que lorsqu'une instance de classe utilisée pour créer un
tr1::shared_ptr<>dérive detr1::enable_shared_from_this<>, la librairie peut reconnaître la relation d'héritage et l'utiliser à son avantage. Nous verrons par la suite comment cela se fait en pratique.
Utilisation
Un tr1::shared_ptr<> est un pointeur autogéré qui peut être partagé entre de multiples instances. Tant que l'une de ces instances est valide, le pointeur reste valide. Lorsque toutes les instances ont été détruites, le pointeur est lui aussi détruit.
Il est possible de copier des tr1::shared_ptr<>, ce qui rend cet objet compatible avec les conteneurs de la librairie standard. On peut ainsi créer une liste ou un vecteur de ces pointeurs:
std::vector<std::tr1::shared_ptr<a_class> > vector_of_a_class_object;
En comparaison, cela n'est pas possible avec std::auto_ptr<>. Considérons le cas suivant: soit v1 et v2 deux vecteur de std::auto_ptr<>. Que se passe-t-il lorsque j'exécute le code v1 = v2; ? La réponse est simple : à cause de la sémantique de transfert de propriété de la classe std::auto_ptr<>, le vecteur source (v2) sera vidé (v[i].get() == 0 quel que soit i). Il n'y a pas moyen d'éviter ce problème.
En utilisant des instances de tr1::shared_ptr<>, il en va autrement: la responsabilité de chaque pointeur sera partagée entre les instances présentes dans v1 et dans v2. De plus, v1[i].get() == v2[i].get() tant que ces valeurs ne sont pas changées. A la destruction de l'un des vecteurs, les pointeurs ne seront pas détruits, et l'autre vecteur sera donc encore entièrement valide.
Bien évidemment, on peut utiliser tr1::shared_ptr<> dans d'autres circonstances, notamment pour gérer la durée de vie d'un pointeur. Ainsi, le considérons le code suivant:
void f()
{
std::tr1::shared_ptr<T> t(new T);
do_something_with_t(t);
}
Pas besoin de gérer la destruction de l'instance de T sous-jacente - le destructeur de tr1::shared_ptr<> s'en chargera. On notera que cette utilisation n'est similaire à celle d'auto_ptr que si do_something_with_t() prend une référence sur un tr1::shared_ptr<>. Si ce n'est pas le cas (c'est à dire si la fonction do_something_with_t() provoque une copie de son paramètre), le mécanisme d'utilisation de l'auto_ptr devient plus complexe - au risque de se perdre - tandis que le shared_ptr continue d'être utilisé de la même manière, le plus naturellement du monde.
Un membre intéressant de tr1::shared_ptr<> est la conversion automatique vers unspecified-bool-type, un type qui peut être converti en booléen par le compilateur (le TR1 propose d'utiliser un type "pointeur sur méthode"; le but est d'éviter les transtypages automatiques vers void* ou bool, qui peuvent être difficiles à contrôler et avoir des effets pervers). Le but de cette conversion est de vérifier la validité du tr1::shared_ptr<>:
std::tr1::shared_ptr<T> sp = std::tr1::shared_ptr<T>();
if (sp) { sp->do_something(); }
Ce code est équivalent à :
T *p = NULL;
if (p) { p->do_something(); }
Tout est fait pour que les instances de tr1::shared_ptr<> s'utilisent de la même manière que les pointeurs qu'elles encapsulent.
Un tr1::weak_ptr<> ne s'utilise pas de la même manière - premièrement, on ne crée pas de tr1::weak_ptr<> à partir d'un pointeur (cela n'aurait aucun intérêt puisque la classe n'assure aucune gestion du pointeur en question). On le crée à partir d'une instance de tr1::shared_ptr<> ou d'un autre tr1::weak_ptr<>. La création d'un tr1::weak_ptr<> n'augmente pas le use_count() du pointeur, pas plus que sa destruction ne le diminue. En fait, au final, on se sert de cette classe pour encapsuler un pointeur glorifié - un pointeur dont on ne contrôle pas la durée de vie mais auquel on a attaché un certain nombre de propriétés.
Parmi ces propriétés, une des plus intéressantes est tr1::weak_ptr<>::expired(), qui renvoie true si le pointeur encapsulé n'est plus valide (c'est à dire si tous les tr1::shared_ptr<> partageant ce pointeur ont été détruits). On est alors capable de savoir si un pointeur peut être utilisé ou non:
{
std::tr1::shared_ptr<T> sp(new T);
std::tr1::weak_ptr<T> wp(sp);
sp.reset(new T);
assert(wp.expired() == false);
}
L'utilisation de tr1::enable_shared_from_this<> est particulière. Comme il a été déjà énoncé, cette classe permet de construire une instance de tr1::shared_ptr<> à partir d'une instance de la classe non encapsulée. Il existe toutefois une restriction importante à cela: un tr1::shared_ptr<> encapsulant cette instance doit exister au préalable. Ainsi, le code suivant n'a pas un comportement défini (et peut donc provoquer des erreurs d'exécution):
class A : public std::tr1::enable_shared_from_this<A>
{ /* ... */ };
void f()
{
A *p = new A;
std::tr1::shared_ptr<A> sp = a->shared_from_this();
}
L'idée d'utilsation est la suivante:
void g()
{
std::tr1::shared_ptr<A> sp(new A);
// ...
f(sp.get());
// ...
}
void f(A* a)
{
std::tr1::shared_ptr<A> sp = a->shared_from_this();
// ...
}
Lors de la création d'un tr1::shared_ptr<>, l'implémentation peut détecter le fait que l'instance dérive de tr1::enable_shared_from_this<> et l'utiliser à son avantage - on verra dans l'étude de l'implémentation comment cela est réalisé.
La suite dans un prochain numéro...
Commentaires
1. Le dimanche 20 avril 2008 à 14:45, par compositeur
2. Le lundi 21 avril 2008 à 09:46, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire