Smart pointers ************** .. include:: ../BoutonGoogleTrad.rst L'origine du problème ===================== Pourquoi évoquer les pointeurs dans un cours de C++ moderne, alors que le langage s’efforce justement de les masquer ? Prenons un exemple simple : une collection d’objets appartenant à une même hiérarchie de classes. Imaginons une classe mère *A* et une classe fille *B*. Si l’on écrit *vector* pour stocker des objets de type *A* et *B*, un problème apparaît. En effet, un *vector* ne peut contenir que des objets de type *A*. Le compilateur acceptera de stocker un objet de type *B*, mais uniquement en le tronquant pour le convertir vers le type *A*, démarche plutôt expéditive ! La solution consiste à utiliser un vector de pointeurs vers des objets de type *A*, le type de la classe mère. Dans ce cas précis, lorsqu'un pointeur est utilisé, le langage vérifie dynamiquement à l'exécution le type réel de l'objet. Ainsi, tant que vous utilisez l’héritage, vous ne pouvez pas bannir complètement les pointeurs. En revanche, dans l’esprit du C++ moderne, il est recommandé de les gérer via des smart pointers. Ceux-ci combinent la performance du C++ avec une gestion mémoire plus sûre, comparable à celle d’un garbage collector en Java ou C#. Les shared_ptr ============== Nous rappelons que : .. panels:: :column: col-lg-5 p-2 .. raw:: html

Les mots-clefs new et delete SONT INTERDITS

.. raw:: html

Les fonctions malloc et free SONT INTERDITES

Si les opérations new/delete ne sont pas autorisées, alors comment que faire ? Nous allons vous présenter une nouvelle approche appelée **pointeurs intelligents** (**smart pointers**). Nous nous concentrons plus particulièrement sur les **shared pointers** qui allient l'efficacité des pointeurs avec la sécurité d'un garbage collector. En effet, un *shared_ptr* est un pointeur doublé d'un **compteur d'utilisation** (**use count**) vers l'entité désignée. Lorsque ce compteur atteint 0, alors le *shared_ptr* déclenche la destruction de l'objet pointé comme le ferait un garbage collector. Shared_ptr comme arguments ========================== Pour instancier un objet de type *T* et l'associer à un *shared pointer* *p*, vous **devez** utiliser la syntaxe suivante : .. panels:: :column: col-lg-9 p-2 | shared_ptr p = **make_shared** (paramètres du constructeur); | ou | **auto** p = make_shared(paramètres du constructeur); Le mot clef *auto* permet de simplifier la syntaxe des instanciations. Pour accéder aux membres de l'objet associé au smart pointeur, il faut utiliser l'opérateur flèche **->** i.e. la notation classique des pointeurs. Voici un exemple ci-dessous : .. code-block:: #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { } void Aff() { cout << "(" << x << "," << y << ")"; } }; int main() { shared_ptr p1 = make_shared(1, 2); p1->Aff(); auto p2 = make_shared(4, 5); p2->Aff(); } >> (1,2) >> (4,5) .. warning:: Nous vous demandons d'utiliser les *shared_ptr* car ils constituent le bagage minimum d'un programmeur C++ moderne. Est-ce que les shared_ptr résolvent tous les problèmes de libération de mémoire ? Malheureusement non. Par exemple, nous pouvons citer le cas des références cycliques : un objet *K* ayant un shared_ptr vers un objet *M* contenant lui-même un shared_ptr vers l'objet *K*. Le mécanisme des shared_ptr reste impuissant car chaque compteur d'utilisation ne pourra pas atteindre 0 à cause du cycle. Nous vous conseillons d'éviter cette situation. Shared_ptr et arguments ======================= Voici un exemple qui indique comment un *shared_ptr* se comporte lors des appels de fonction : .. code-block:: cpp #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { cout << "Point created\n"; } ~Point() { cout << "Point destroyed\n"; } void Print() { cout << "(" << x << "," << y << ")" << endl; } }; void test_1(shared_ptr & p1) { // pass by reference 5 cout << "test_1 use count : " << p1.use_count() << "\n"; } void test_2(shared_ptr p2) { // pass by copy 7 cout << "test_2 use count : " << p2.use_count() << "\n"; } int main() { 1 auto p = make_shared(1, 2); // instantiation 2 p->Print(); 3 cout << "use count : " << p.use_count() << "\n"; 4 test_1(p); 6 test_2(p); 8 cout << "use count : " << p.use_count() << "\n"; } 1 >> Point created 2 >> (1,2) 3 >> use count : 1 5 >> test_1 use count : 1 7 >> test_2 use count : 2 8 >> use count : 1 9 >> Point destroyed Description ligne à ligne : * 1 : *auto p = make_shared(1, 2)* : crée un objet de type *Point* et un *shared_ptr p* pointant sur cet objet. * 2 : *p->Aff();* : affiche les coordonnées *(x,y)* du point en question. * 3 : *cout << "use count"* : un seul *shared_ptr* point vers l'objet Point, le compteur est à 1. * 4 : *test_1(p)* : appel de la fonction *test_1*, le shared_ptr est passé par référence. * 5 : *cout << "test_1"*, il n'y a pas eu de nouveau pointeur créé, le compteur est toujours égal à 1. * 6 : *test_2(p)* : appel de la fonction *test_2*, le *shared_ptr* est passé par copie, un nouveau *shared_ptr p2* est créé et désigne le même objet. * 7 : *cout << "test_2"*, les deux *shared_ptr* *p* et *p2* pointent vers le même objet, le compteur vaut donc 2. * 8 : *cout << "use count"*, au retour dans la fonction *test_2*, le *shared_ptr p2* a été détruit, le compteur retombe à 1. * 9 : fin de la fonction *main()*, le *shared_ptr p* est détruit, le compteur de tombe à 0, et l'objet *Point* est détruit automatiquement. Shared_ptr et vector ==================== Voici un exemple qui indique comment un *shared_ptr* se comporte avec des *vector* : .. code-block:: cpp #include #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { cout << "Point created : "; Print(); } ~Point() { cout << "Point destroyed: "; Print(); } void Print() { cout << "(" << x << "," << y << ")" << endl; } }; int main() { vector> L; 1 L.push_back(make_shared(1,2)); 1 L.push_back(make_shared(3,4)); 2 L.push_back(L[0]); 3 cout << "count L[0] : " << L[0].use_count() << "\n"; 4 cout << "count L[1] : " << L[1].use_count() << "\n"; 5 cout << "L[1] destroyed" << endl; 6 L.erase(L.begin()+1); 7 cout << "End of program" << endl; } 1 >> Point created : (1,2) 1 >> Point created : (3,4) 3 >> use count L[0] : 2 4 >> count L[1] : 1 5 >> L[1] destroyed 6 >> Point destroyed: (3,4) 7 >> End of program >> Point destroyed: (1,2) Commentaires : * 1 : Deux objets *Point* sont créés et insérés dans un *vector L*. * 2 : Le *shared_ptr* *L[0]* est copié pour créer un 3ème élément dans la liste *L*. * 3 : Le compteur d'utilisation vaut 2 car il y a deux *shared_ptr* pointant vers le même objet : *L[0]* et *L[2]*. * 4 : Le compteur de *L[1]* vaut 1 car il y a un unique *shared_ptr* pointant vers cet objet. * 5 : La ligne : *L.erase(L.begin()+1)* efface le *shared_ptr L[1]*, le compteur tombe à 0. * 6 : Par conséquent le *Point : (3,4)* est détruit dans la foulée : *Point destroyed : (3,4)* * 7 : La fonction *main* se termine, la liste *L* est détruite ainsi que les *shared_ptr* qu'elle contient. * 8 : Le compteur d'utilisation vers l'objet *Point(1,2)* tombe à 0 et cet objet est automatiquement détruit : *Point destroyed : (1,2)*. Quizzz ====== .. quiz:: smartp :title: Smart pointers * :quiz:`{"type":"FB","answer":"make_shared(3,5)"}` Syntaxe pour créer un shared pointer sur un objet de type *T* avec comme paramètres de construction 3 et 5. Ne pas écrire le ; * :quiz:`{"type":"TF","answer":"F"}` Depuis un shared pointer *p*, pour accéder au paramètre *a*, dois-je écrire *p.a* ? * :quiz:`{"type":"FB","answer":"1"}` Quelle est la valeur affichée par le code suivant : .. code-block:: void test(shared_ptr p) { p->Aff(); } int main() { shared_ptr p = make_shared(1,2); for (int i = 0 ; i < 3 ; i++) test(p); cout << p.use_count(); return 0; } * :quiz:`{"type":"TF","answer":"F"}` Un shared pointer permet de libérer l'objet pointé dès que son compteur d'utilisation vaut -1.