Notez que j'appelais autrefois cette classe SmattPointer, mais que j'ai choisi de rapprocher mon nom de noms similaires à ceux du standard du langage C++, ce qui explique la différence entre le nom de cette page Web et son contenu.
L'idée derrière la programmation par politiques (une idée de Andrei Alexandrescu, à ma connaissance) est, en gros :
Le recours à un pointeur intelligent suggère que le comportement de l'objet englobant ressemble au comportement d'un pointeur (déréférencement à travers l'opérateur * unaire et accès aux membres à travers l'opérateur ->).
Je vous invite à examiner le code de l'opérateur -> (un animal assez unique dans le zoo de C++) et à y réfléchir : il est très subtil...
Mon exemple est un pointeur intelligent plus ou moins brillant qui encapsule un pointeur en sachant, à travers des politiques, s'il doit procéder par clonage ou à l'aide d'une construction par copie lorsqu'il doit dupliquer son pointé.
Je vous rappelle que le clonage sied bien aux types polymorphiques (surtout s'ils sont manipulés à travers des abstractions pures comme des classes abstraites ou des interfaces) alors que la construction par copie sied bien aux types valeur (std::string, std::vector, int, etc.).
Ceci explique le recours fréquent au clonage en Java (systématiquement!) et dans les langages .NET (très fréquemment) : en Java comme en .NET, tous les types objets sont manipulés à travers des références et en Java, tous les types sont polymorphiques (du moins par défaut).
Notez que dans ce cas, les classes Copieur et Cloneur sont sans états. L'approche suivie est d'utiliser une instance de l'une ou de l'autre dans un dup_ptr, pour solliciter ses services lorsque vient le moment de dupliquer un T. En utilisant un attribut, nous occupons toutefois de l'espace dans le dup_ptr. Dans ces cas bien précis, deux alternatives auraient été envisageables :
Utiliser un attribut de type Dup permet par contre d'avoir des types de « duplicateurs » munis d'états, ce qui pourrait être le choix du code client (dans le cas où un Dup serait explicitement spécifié par le code client), mais dans la mesure où le type Dup en question est copiable (et déplaçable). C'est pour cette raison que j'ai appliqué cette approche. |
|
Quelques remarques :
|
|
La méthode get() est un compromis. On trouve de tels compromis régulièrement dans le code de pointeurs intelligents, du fait qu'ils permettent entre autres d'implémenter des mécanismes de covariance |
|
Il va de soi que nous souhaiterons exposer un constructeur paramétrique, permettant de confier à un dup_ptr<T> un pointeur à prendre en charge, et donc de jouer son rôle. Il est sage de qualifier le constructeur paramétrique d'explicite. |
|
La Sainte-Trinité doit être implémentée, pour que dup_ptr<T> puisse effectivement implémenter la sémantique de duplication attendue (sur la base de pa politique de duplication que représente dup). Remarquez que p n'est pas construit en préconstruction... Pourquoi donc? |
|
Sémantique de mouvement. Ici, j'ai choisi de copier le Dup, mais c'est un choix qui peut être débattu. |
|
« Conversion » (utile pour mettre un Ferrari* dans un Voiture*). Notez que par souci d'orthogonalité, j'ai ajouté un mécanisme de duplication de « duplicateur » (méthode duplicateur()). Une question profonde qui mérite une attention allant bien au-delà de ce bref exposé, est celle de la possibilité de copier un dup_ptr<T,D0> dans un dup_ptr<T,D1>, ou encore de copier un dup_ptr<T,DT> dans un dup_ptr<U,DU>. |
|
Comparer deux dup_ptr. Je ne tiens pas compte des « duplicateurs » ici, car il ne s'agit pas de propriétés manifestes du type. |
|
Accéder au pointé demande les services « de base » d'un pointeur, que voici. |
|
Tester un dup_ptr comme un booléen est bien sûr un service utile... |
|
... tout comme l'est la comparaison d'un dup_ptr avec un pointeur nul |
|
La fonction std::forward et les templates variadiques sont avantageusement appliqués au relais des paramètres... Je vous invite à explorer le Perfect Forwarding, ça en vaut la peine! |
|
Quelques remarques sur des éléments particuliers de notre pointeur intelligent suivent.
Dans le cas du constructeur de conversion, il faut que le type en cours de construction (dup_ptr<T,Dup>) ait accès au pointeur dans le type d'origine (dup_ptr<U,Dup>), or ces deux types sont distincts, donc l'un n'a pas accès aux membres privés de l'autre.
On aurait pu envisager de remplacer ce service par un autre tel que :
//
// ...
//
template <class D>
T* co_dup(D dup) {
return dup(p);
}
//
// ...
//
template <class U>
dup_ptr(const dup_ptr<U> &p) : p{} {
p = p.co_dup(dup);
}
//
// ...
//
Cela éliminerait le bris d'encapsulation provoqué par get() mais aurait été plus subtil à expliquer, disons-le ainsi.
Une alternative plus moderne serait de faire de tout dup_ptr<U,D> un ami de dup_ptr<T,Dup>, chose qui n'a pas toujours été possible en C++. Pour ce faire, il suffit désormais de faire :
template <class U, class D>
friend class dup_ptr;
Ceci éliminerait l'obligation d'offrir des services de duplication du duplicateur et d'accès direct au pointeur brut (service get()). Il est probable que get() demeure utile en pratique, pour utiliser des fonctions pensées pour des pointeurs bruts.
Il est d'usage de déclarer explicit les constructeurs paramétriques de types tels que les pointeurs intelligents, pour lesquels il pourrait y avoir des conséquences. Ici, en forçant le code client à spécifier explicitement qu'il souhaite construire un dup_ptr, nous le forçons à réfléchir sur le geste qu'il pose et sur ses conséquences.
Notez que le constructeur de copie est écrit sous une forme qui demande un soin particulier.
Faites ceci | Ne faites pas cela |
---|---|
|
|
La raison de cet appel à la prudence est que la version de droite exige que la construction de dup précède la construction de p, chose qui dépend de l'ordre de déclaration des attributs dans dup_ptr. Ce type de dépendance obscure est à éviter, règle générale, vous le comprendrez. La même remarque s'applique d'ailleurs au constructeur de conversion.
Dépendre d'une interface spécifique est une approche qui fonctionne, mais il s'agit aussi d'une approche intrusive, qu'il est souvent préférable d'éviter pour réduire le couplage dans un design.
Supposons que, plutôt qu'exiger l'implémentation d'une interface Clonable spécifique pour un type T comme nous le faisons avec is_convertible_v<T*,Clonable*> plus haut, nous souhaitions détecter la présence d'une méthode cloner() qui soit const dans le type T, et que nous souhaitions en faire un trait est_clonable<T>.
Il existe plusieurs manières d'y arriver, mais l'une des plus charmantes est sûrement ce que le brillant Walter Brown a nommé le C++ Detection Idiom : le trait std::void_t. Voir ../Sujets/Divers--cplusplus/void_t.html pour plus de détails (ce qui suit n'est qu'une application concrète de la technique).
Le type std::void_t sur lequel repose cette technique est logé dans l'en-tête standard <type_traits>. |
|
Le technique repose sur la mise en place d'un trait est_clonable général et de sa spécialisation :
Le compilateur choisira la spécialisation si l'expression est correctement construite pour un type T donné, et l'exclura dans le cas contraire pour choisir le cas général. |
|
La constante est_clonable_v<T> est un raccourci pour l'expression est_clonable<T>::value. C'est utile, sans être nécessaire |
|
Il est facile de valider le tout. Soit deux types X et Y tels que X expose une méthode cloner() et Y n'en expose pas. Le code de test qui suit compile sans problème. |
|
Un programme y ayant recours serait le suivant. Dans ce programme :
#include <iostream>
struct Objet3D : Clonable {
virtual Objet3D *cloner() const override = 0;
virtual ~Objet3D() = default;
virtual void dessiner() const = 0;
protected:
Objet3D() = default;
Objet3D(const Objet3D &obj) : Clonable{ obj } {
}
};
struct Nuage : Objet3D {
Nuage *cloner() const override {
return new Nuage;
}
void dessiner() const override {
using namespace std;
cout << "Ooohh.... Un nuage..." << endl;
}
protected:
Nuage() = default;
Nuage(const Nuage &obj) noexcept : Objet3D{obj} {
}
};
#include <string>
class Voiture : public Objet3D {
std::string nom_;
public:
Voiture(const std::string &nom) : nom_{ nom } {
}
Voiture *cloner() const override {
return new Voiture{*this};
}
virtual void dessiner() const override {
using namespace std;
cout << "Vroum! (" << nom_ << ")" << endl;
}
void renommer(const std::string &nom) {
nom_ = nom;
}
const std::string& nom() const noexcept {
return nom_;
}
protected:
Voiture(const Voiture &obj) : Objet3D{obj}, nom_{obj.nom()} {
}
};
int main() {
using namespace std;
auto p = make_dup<Voiture>("Bazou"); // p est dup_ptr<Voiture>
p->dessiner();
auto q = p;
q->dessiner();
q->renommer("Citron");
p->dessiner();
q->dessiner();
auto r = make_dup<std::string>("J'aime mon prof");
auto s = r;
cout << '\"' << *r << '\"' << " et " << '\"' << *s << '\"' << endl;
*s = "Yo";
cout << '\"' << *r << '\"' << " et " << '\"' << *s << '\"' << endl;
}
Voilà!