Sobes.tech
Middle
212

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:

  1. Dépendances cycliques (propriété partagée au lieu de propriété unique): Si deux unique_ptr possè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 de unique_ptr comme propriétaire exclusif. Pour ces cas, il est préférable d'utiliser shared_ptr avec weak_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.
    }
    
  2. Exceptions lors de la création de l'objet: Si lors de la création d'un objet que unique_ptr va 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;
    }
    
  3. Déleter incorrect: Si unique_ptr est 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.
    }
    
  4. Appel à .release() sans delete ulté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.