Gestion des blocs de contrôle multiples lors de la création de shared_ptr à partir de this en C++

La création directe d'un std::shared_ptr à partir du pointeur this au sein d'une classe peut entraîner la création de blocs de contrôle de référence distincts, manant à des suppressions multiples et à un comportement indéfini.


class MaClasse {
public:
    void partager() {
        std::shared_ptr<MaClasse> ptr_partage(this); // Dangereux !
    }
};

Si l'objet est déjà géré par un shared_ptr, cette approche crée un second bloc de contrôle indépendant, ce qui conduit à une double suppression de l'objet.

La méthode recommandée consiste à hériter de std::enable_shared_from_this<MaClasse> et à utiliser la méthode shared_from_this().

std::enable_shared_from_this<T>

std::enable_shared_from_this<T> est un modèle de classe de base utilitaire qui permet à un objet géré par std::shared_ptr de générer en toute sécurité un shared_ptr qui pointe vers lui-même.

Lorsque vous avez un objet actuellement géré par un shared_ptr et que vous devez retourner un shared_ptr pointant vers this depuis une fonction membre, vous ne devez pas utiliser directement return std::shared_ptr<T>(this).

Cela créerait plusieurs blocs de contrôle de référence indépendants à partir du même pointeur brut, ce qui entraînerait une suppression répétée de l'objet (comportement indéfini).


class Dangereux {
public:
    std::shared_ptr<Dangereux> obtenirPointeur() {
        return std::shared_ptr<Dangereux>(this);   // Dangereux !
    }
};

auto ptr1 = std::make_shared<Dangereux>();
auto ptr2 = ptr1->obtenirPointeur();   // ptr1 et ptr2 ont des blocs de contrôle différents, entraînant une double suppression.

enable_shared_from_this fournit un mécanisme sécurisé pour obtenir des shared_ptr supplémentaires à partir d'un bloc de contrôle existant sans en créer de nouveau.

Utilisation

  1. Faites en sorte que votre classe MaClasse hérite publiquement de std::enable_shared_from_this<MaClasse>.
  2. Lorsque vous avez besoin de retourner un shared_ptr pointant vers this, appelez shared_from_this().

#include <memory>
#include <iostream>

class MaClasse : public std::enable_shared_from_this<MaClasse> {
public:
    std::shared_ptr<MaClasse> getPartage() {
        return shared_from_this();   // Sûr
    }

    void afficher() {
        std::cout << "Objet MaClasse" << std::endl;
    }
};

int main() {
    auto ptr_sp1 = std::make_shared<MaClasse>();
    auto ptr_sp2 = ptr_sp1->getPartage();   // ptr_sp2 et ptr_sp1 partagent le même bloc de contrôle.
    std::cout << ptr_sp1.use_count() << std::endl; // Affiche 2
    return 0;
}

Principe de fonctionnement

  • enable_shared_from_this maintient un weak_ptr<MaClasse> interne qui pointe vers le bloc de contrôle de l'objet.
  • Lorsque l'objet est géré pour la première fois par un shared_ptr (par exemple, via std::make_shared ou std::shared_ptr<MaClasse>(new MaClasse())), ce weak_ptr interne est initialisé pour pointer vers ce bloc de contrôle.
  • L'appel à shared_from_this() équivaut à construire un nouveau shared_ptr à partir de ce weak_ptr interne. Cela augmente le compteur de références tout en garantissant l'unicité du bloc de contrôle.

Prérequis

L'objet doit déjà être géré par un shared_ptr.

Avant d'appeler shared_from_this(), vous devez vous assurer que l'objet courant est déjà détenu par au moins un shared_ptr. Sinon, une exception std::bad_weak_ptr sera levée.


MaClasse objet;                     // Objet sur la pile, non géré par shared_ptr
auto ptr = objet.shared_from_this(); // Lève std::bad_weak_ptr

Scénarios d'utilisation

  • Callbacks asynchrones : Nécessité de passer l'objet courant à une tâche asynchrone, garantissant que la durée de vie de l'objet est prolongée jusqu'à la fin de la tâche.
  • Modèle de fabrique : La méthode de fabrique retourne un shared_ptr, et l'objet lui-même doit retourner son propre shared_ptr.
  • Modèle Observateur : L'objet observé doit enregistrer son shared_ptr auprès des observateurs.
  • Pools d'objets : L'objet doit fournir son shared_ptr pour une utilisation externe.

// Exemple de callback asynchrone
class TacheAsynchrone : public std::enable_shared_from_this<TacheAsynchrone> {
public:
    void demarrer() {
        auto moi_ptr = shared_from_this();   // Maintient l'objet en vie
        std::thread([moi_ptr]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            moi_ptr->onTermine();
        }).detach();
    }
    void onTermine() { /* Traiter l'achèvement */ }
};

Points d'attention

  • Ne pas appeler shared_from_this() dans le constructeur. À ce stade, l'objet n'est pas encore pris en charge par un shared_ptr, le weak_ptr interne n'est pas initialisé, et une exception sera levée.
  • Éviter l'héritage multiple de enable_shared_from_this. Il ne faut hériter qu'une seule fois, généralement de la classe la plus dérivée. Faites attention à l'héritage virtuel en cas d'héritage multiple.
  • Coût en performance. Le stockage interne d'un weak_ptr augmente légèrement la taille de l'objet (généralement la taille d'un pointeur), mais le coût est minime.
  • Compatibilité avec std::make_shared. Les objets créés avec make_shared peuvent utiliser shared_from_this sans problème.
  • Ne peut pas être utilisé avec des objets gérés par std::unique_ptr. unique_ptr ne maintient pas de bloc de contrôle de référence, donc il n'y a pas de weak_ptr correspondant.

Exemple : vérification automatique de l'héritage sans gestion par shared_ptr

Pour contraindre au niveau de la conception la création d'objets uniquement via shared_ptr, rendez le constructeur privé et fournissez une méthode de fabrique statique qui retourne un shared_ptr. Ainsi, tout objet sera nécessairement géré par un shared_ptr, éliminant le besoin de vérifications supplémentaires.


#include <memory>

class MaClasse : public std::enable_shared_from_this<MaClasse> {
private:
    MaClasse() = default;                     // Constructeur privé
public:
    static std::shared_ptr<MaClasse> creer() {
        return std::shared_ptr<MaClasse>(new MaClasse());
    }

    void faireQuelqueChose() {
        auto ptr = shared_from_this();        // Sûr, car l'objet est obligatoirement dans un shared_ptr
    }
};

int main() {
    auto obj = MaClasse::creer();            // Création uniquement via la fabrique
    obj->faireQuelqueChose();
    // MaClasse obj2;                         // Erreur : constructeur privé
    return 0;
}

Cependant, cette approche empêche l'allocation d'objets sur la pile ou leur utilisatino comme membres directs d'autres classes sans passer par un shared_ptr.

Étiquettes: C++ shared_ptr enable_shared_from_this Gestion Mémoire pointeurs intelligents

Publié le 24 juillet à 08h29