Comment peut-on provoquer une fuite de mémoire lors de l'utilisation de unique_ptr ?
Réponse de l'IA
sobes.tech IA
C'est difficile, mais possible dans certains scénarios:
-
Dépendances cycliques (propriété partagée au lieu de propriété unique): Si deux
unique_ptrpossèdent des objets qui se réfèrent mutuellement, et aucun d'eux ne se détruit en premier, la mémoire ne sera pas libérée. Cela viole l'idée deunique_ptrcomme propriétaire exclusif. Pour ces cas, il est préférable d'utilisershared_ptravecweak_ptr.#include <memory> struct A; struct B; struct A { std::unique_ptr<B> ptr_b; A() {} ~A() { /* destructeur de A */ } }; struct B { std::unique_ptr<A> ptr_a; // Dépendance cyclique de A B() {} ~B() { /* destructeur de B */ } }; int main() { auto a = std::make_unique<A>(); auto b = std::make_unique<B>(); // Établir des références mutuelles // Cela entraînera une fuite si le propriétaire a ou b ne détruit pas son unique_ptr en premier, // ce qui n'est pas possible dans ce cas en raison de la propriété mutuelle a->ptr_b = std::move(b); // En sortant de main, le unique_ptr<A> a sera détruit, mais son destructeur A // tentera de détruire ptr_b. Cependant, si ptr_b possède un objet B, // et que l'objet B contient un unique_ptr<A> qui possédait l'objet A original, // une dépendance cyclique est créée, menant à une fuite si elle n'est pas gérée avec soin. // Dans cet exemple simplifié avec make_unique, seul a restera dans le scope // et son destructeur sera appelé. La fuite se produira si B possède A, et A possède B. // Ici, b a été déplacé dans a->ptr_b. Le unique_ptr<B> b dans main est maintenant vide. // En sortant de main, a sera détruit. Le destructeur de A sera appelé. // Le destructeur de unique_ptr<B> ptr_b sera appelé. // Le destructeur de B sera appelé. // Le destructeur de unique_ptr<A> ptr_a dans B sera appelé. // Le problème de dépendance cyclique survient lorsque les objets ne peuvent pas se détruire mutuellement // parce que chacun possède l'autre. // Exemple correct de dépendance cyclique: // struct A { std::unique_ptr<B> b; }; // struct B { A* a_raw_ptr; }; // B connaît A, mais ne possède pas // Dans le cas de unique_ptr, la propriété cyclique est extrêmement peu courante et difficile à créer directement // sans erreurs de conception explicites. Un lien cyclique normal ne conduit pas à une fuite par lui-même // avec unique_ptr, s'il n'y a pas de propriété cyclique. // **Scénario réel de fuite avec unique_ptr dans un contexte de cycles:** // Ce n'est pas dû au unique_ptr lui-même, mais à la logique de propriété. // Par exemple, si vous avez une structure qui *stocke* un unique_ptr à une autre structure, // qui à son tour *stocke* un unique_ptr à la première, et que vous créez de tels objets // avec new et les assignez à des unique_ptr: // auto obj1 = std::make_unique<A>(); // auto obj2 = std::make_unique<B>(); // obj1->ptr_b = std::move(obj2); // obj1 possède maintenant obj2 // obj1->ptr_b->ptr_a = std::move(obj1); // <- PROBLÈME: obj2 tente de posséder obj1, // qui possède déjà obj2. // Cela entraînera une erreur à l'exécution ou un unique_ptr non initialisé, // pas une fuite cyclique comme avec shared_ptr. // **Méthode la plus réaliste de fuite avec unique_ptr dans un contexte de dépendances:** // Si vous utilisez un pointeur brut à l'intérieur d'un objet référencé par un unique_ptr, // et que ce pointeur brut pointe vers un objet possédé par un autre unique_ptr, // et que vous oubliez de libérer le pointeur brut lorsque le deuxième unique_ptr est détruit en premier. // Mais ce n'est pas une fuite de unique_ptr, c'est une fuite du pointeur brut ou une mauvaise utilisation de mémoire. // Concentrons-nous sur comment le propre unique_ptr peut contribuer à une fuite, // et non sur des erreurs de conception en dehors de lui. // La façon la plus directe est par exception. } -
Exceptions lors de la création de l'objet: Si lors de la création d'un objet que
unique_ptrva posséder, ou pendant le constructeur de cet objet, une exception survient après l'allocation de mémoire (new T()), mais avant l'assignation de cette mémoire àunique_ptr.#include <memory> class Resource { public: Resource() { // Si une exception est levée ici... throw std::runtime_error("Erreur dans le constructeur de Resource"); } ~Resource() { // ce destructeur ne sera pas appelé si l'exception survient dans le constructeur } }; int main() { Resource* res = nullptr; try { res = new Resource(); // mémoire allouée // ... mais une exception survient avant que make_unique ne prenne possession // std::unique_ptr<Resource> unique_resource(res); // <- ce code ne sera pas atteint std::cout << "Cette ligne ne sera pas exécutée" << std::endl; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; // `res` pointe vers la mémoire allouée, mais elle n'a jamais été assignée // à un `unique_ptr`, ni libérée. Fuite. // La bonne pratique serait: delete res; // dans le bloc catch } // Fuite de mémoire si delete res; n'est pas appelé dans le catch // **Comment éviter la fuite dans ce cas:** // Utilisez std::make_unique try { std::unique_ptr<Resource> safe_resource = std::make_unique<Resource>(); // RAII // Si une exception survient dans le constructeur de Resource, make_unique // gère correctement la mémoire allouée. } catch (const std::exception& e) { std::cerr << "Exception gérée avec make_unique: " << e.what() << std::endl; // Pas de fuite } return 0; } -
Déleter incorrect: Si
unique_ptrest configuré avec un deleter personnalisé qui ne fait pas son travail.#include <memory> #include <iostream> struct MyData { int value; MyData(int v) : value(v) { std::cout << "MyData(" << value << ") créé" << std::endl; } ~MyData() { std::cout << "MyData(" << value << ") détruit" << std::endl; } }; // Deleter personnalisé qui *ne* supprime pas la mémoire struct NoOpDeleter { void operator()(MyData* ptr) const { std::cout << "NoOpDeleter appelé pour " << ptr->value << ", mais sans supprimer!" << std::endl; // delete ptr; // <- Oubli de décommenter ou suppression intentionnelle } }; int main() { // Création d'un unique_ptr avec deleter personnalisé std::unique_ptr<MyData, NoOpDeleter> data_ptr(new MyData(10)); // À la sortie du scope, NoOpDeleter sera appelé. // Comme NoOpDeleter ne supprime pas, la mémoire allouée avec new MyData(10) ne sera pas libérée. Fuite. return 0; // ~unique_ptr() appelle NoOpDeleter::operator(), mais la mémoire n'est pas supprimée. } -
Appel à
.release()sansdeleteultérieur:release()transfère la propriété du pointeur brut. Si ce pointeur n'est pas ensuite explicitement supprimé (ou transféré à un autre smart pointer), il y aura une fuite.#include <memory> #include <iostream> struct LeakyResource { LeakyResource() { std::cout << "LeakyResource créé" << std::endl; } ~LeakyResource() { std::cout << "LeakyResource détruit" << std::endl; } }; int main() { std::unique_ptr<LeakyResource> ptr = std::make_unique<LeakyResource>(); LeakyResource* raw_ptr = ptr.release(); // ptr devient nullptr // La propriété est transférée à raw_ptr. // La mémoire pointée par raw_ptr ne sera pas automatiquement libérée // à la sortie de main. // ... travail avec raw_ptr ... // Oubli de supprimer! // delete raw_ptr; // <- Fuite si cette ligne est absente std::cout << "ptr est " << (ptr ? "non null" : "null") << std::endl; // Affichera "ptr est null" return 0; // raw_ptr pointe toujours vers la mémoire allouée, mais elle n'est pas libérée }
Dans tous les autres scénarios standard, unique_ptr, basé sur RAII, gère correctement la mémoire, la libérant à la sortie du scope.