3 votes

Laisser le drapeau "zombie" lors de la destruction de l'objet

Je veux détruire explicitement un objet (appeler le destructeur sur lui et tous ses champs), mais il se peut que je détienne encore des pointeurs vers l'objet en question. Ainsi, je ne veux pas encore libérer la mémoire ; à la place, je voudrais laisser une sorte de drapeau "je suis un objet détruit".

J'ai eu l'idée de l'approche suivante :

class BaseClass { //all objects in question derive from this class
public:
    BaseClass() : destroyed(false) {}
    virtual ~BaseClass() {}
private:
    bool destroyed;
public:
    bool isDestroyed() { return destroyed; }
    void destroy() {
        this->~BaseClass(); //this will call the virtual destructor of a derivative class
        new(this) BaseClass();
        destroyed=true;
    }
};

Lorsque destroy est appelé, je détruis l'objet que j'avais (peut-être un objet dérivé) et je crée un nouvel objet "zombie" au même endroit. Le résultat que j'espère obtenir :

  • Tout autre pointeur ptr qui pointaient auparavant sur cet objet peuvent encore appeler ptr->isDestroyed() pour vérifier son existence.
  • Je suis conscient que si je ne vérifie pas le drapeau du zombie et que j'essaie d'accéder aux champs appartenant à un objet dérivé, de mauvaises choses peuvent se produire
  • Je suis conscient que l'objet zombie consomme toujours autant de mémoire que l'objet détruit (car il peut être un dérivé de BaseClass )
  • Je dois encore libérer la mémoire de l'objet détruit. J'espère cependant que l'appel delete est toujours correcte ?

Questions :

Y a-t-il d'autres problèmes dont je dois tenir compte lorsque j'utilise le modèle ci-dessus ?

Appel de la volonté delete sur l'objet zombie libère correctement toute la mémoire consommée par l'objet précédent (normal) ?


Bien que j'apprécie votre contribution sur la façon de procéder différemment, et que je sois peut-être enclin à le faire à votre façon, je voudrais quand même comprendre tous les risques que le code ci-dessus pose.

5voto

Branko Dimitrijevic Points 28493

Il est suggéré d'utiliser des pointeurs intelligents. En fait, c'est ce que je fais, mais mes références sont circulaires. Je pourrais utiliser des collecteurs d'ordures à part entière, mais comme je sais moi-même où (et quand !) les chaînes circulaires peuvent être brisées, je veux en tirer parti moi-même.

Ensuite, vous pouvez explicitement nullifier ( réinitialiser si vous utilisez shared_ptr ) l'un des pointeurs intelligents circulaires et rompre le cycle.

Par ailleurs, si vous savez à l'avance où se trouvent les cycles, vous devriez également pouvoir éviter à l'avance en utilisant weak_ptr à la place de certains des shared_ptr s.

--- EDIT ---

Si un objet référencé uniquement par des pointeurs faibles était simplement signalé comme "invalide" et perdait le contrôle de tous les pointeurs qu'il contient (cette phrase est-elle claire ?), je serais heureux.

Puis weak_ptr::expired devrait vous rendre heureux :)

5voto

Sebastian Cabot Points 1690

Vous avez reçu des commentaires désagréables à votre question. Maintenant, je ne pense pas qu'ils soient mérités, bien qu'il puisse y avoir de meilleures façons de faire ce que vous voulez. Je comprends votre point de vue, mais en fait vous utilisez le destructeur de la même manière que la fonction reset que vous refusez d'écrire. En fait, vous ne gagnez rien à appeler un destructeur puisque l'appel d'un destructeur n'a rien à voir avec la suppression ou la réinitialisation de quoi que ce soit, à moins que vous n'écriviez le code pour le faire dans le destructeur.

Quant à votre question sur le nouveau placement :

Comme vous le savez peut-être déjà, le placement new n'alloue pas de mémoire, donc l'appeler créera simplement l'objet au même endroit. Je comprends que c'est exactement ce que vous voulez mais ce n'est pas nécessaire. Puisque vous n'appelez pas delete sur votre objet mais seulement destroy, vous pouvez mettre destroyed à true sans initialiser la classe.

Pour résumer :

  1. Si vous utilisez le destructeur comme une fonction virtuelle ordinaire, vous ne gagnez rien. Ne le faites pas car vous pouvez avoir des problèmes si un destructeur est appelé deux fois.
  2. Un appel à un placement new n'allouera pas de mémoire et ne fera qu'une initialisation inutile. Vous pouvez simplement mettre destroyed à true.

Pour faire ce que vous voulez faire correctement et bénéficier des avantages des destructeurs, vous devez surcharger les opérateurs new et delete de vos classes et utiliser le mécanisme de destruction normal. Vous pouvez ensuite choisir de ne pas libérer la mémoire mais de la marquer comme invalide ou peut-être de libérer la majeure partie de la mémoire mais de laisser le pointeur pointer vers certains drapeaux.

EDIT

Suite aux commentaires, j'ai décidé de résumer tous les risques que je vois et ceux que d'autres ont signalés :

  1. Accès à un pointeur invalide dans un environnement multithread : En utilisant votre méthode, on peut accéder à une classe après l'exécution du destructeur mais avant que l'indicateur de destruction ne soit activé (pour répondre à votre question dans l'un des commentaires - shared_ptr est pour la plupart des cas thread safe).
  2. S'appuyer sur un comportement que l'on ne contrôle pas totalement : Votre méthode s'appuie sur la façon dont les destructeurs appellent automatiquement les destructeurs d'autres membres qui ne sont pas alloués dynamiquement : Cela signifie que vous devez toujours libérer la mémoire allouée dynamiquement spécifiquement, Vous n'avez aucun contrôle sur la façon exacte dont cela est mis en œuvre, Vous n'avez aucun contrôle sur l'ordre dans lequel les autres destructeurs sont appelés.
  3. Relayer un comportement que vous ne maîtrisez pas totalement (point 2) : Vous vous appuyez sur la façon dont un compilateur implémente la partie du destructeur qui appelle d'autres destructeurs ; vous n'avez aucun moyen de savoir si votre code sera portable ou même comment il gérera le fait de l'appeler deux fois.
  4. Les destructeurs peuvent être appelés deux fois : selon votre implémentation, cela peut provoquer des fuites de mémoire ou une corruption du tas, à moins que vous ne vous prémunissiez contre la libération de la même mémoire deux fois. Vous prétendez vous prémunir contre ce cas en appelant le placement new - Cependant, dans un environnement multithreading, cela n'est pas garanti ; de plus, vous supposez que toutes les allocations de mémoire sont effectuées par le constructeur par défaut - selon votre implémentation spécifique, cela peut être vrai ou non.
  5. Vous allez à l'encontre du meilleur jugement de tous ceux qui ont répondu à votre question ou l'ont commentée. Vous êtes peut-être sur quelque chose de génial, mais vous vous tirez probablement une balle dans la jambe en limitant votre mise en œuvre à un petit sous-ensemble de situations où elle fonctionnera correctement. C'est comme si vous utilisiez le mauvais tournevis et que vous finissiez par endommager la vis. De la même manière, l'utilisation d'une construction du langage d'une manière qui n'était pas prévue peut aboutir à un programme bogué - Les destructeurs sont destinés à être appelés à partir de delete et du code généré par le compilateur pour vider la pile. Il n'est pas judicieux de les utiliser directement.

Et je répète ma suggestion - surchargez supprimer et nouveau pour ce que vous voulez.

2voto

eh9 Points 3804

Comme pour tous les autres, je vous recommande d'utiliser simplement weak_ptr . Mais vous avez demandé pourquoi votre approche ne fonctionne pas aussi bien. Il y a quelques problèmes d'implémentation élégante et de séparation des préoccupations que votre code ne prend pas en compte, mais je ne les discuterai pas. Au lieu de cela, je vais simplement souligner que votre code est horriblement pas thread-safe.

Considérons la séquence d'exécution suivante de deux threads de contrôle :

// Thread One
if ( ! ptr -> isDestroyed() ) {     // Step One
    // ... interruption ...
    ptr -> something();             // Step Three

Et l'autre :

// Thread Two
ptr -> destroy();                   // Step Two

Au moment de l'étape 3, le pointeur n'est plus valable. Il est maintenant possible de résoudre ce problème en mettant en œuvre la méthode suivante lock() ou similaire, mais maintenant vous avez encouru la possibilité de défauts sur le fait de ne pas libérer les verrous. La raison pour laquelle tout le monde recommande weak_ptr est que toute cette catégorie de problèmes a été résolue à la fois dans l'interface de la classe et dans ses implémentations.

Une question demeure. Vous semblez vouloir une installation où vous pouvez tuer un objet à volonté. Cela revient à exiger que les seuls pointeurs vers un objet soient des pointeurs faibles, qu'il n'y ait pas de pointeurs forts qui se briseraient si l'objet était supprimé manuellement. (Je stipulerai que ce n'est pas une mauvaise idée, même si je dois dire que je ne sais pas pourquoi ce n'est pas le cas dans votre cas). Vous pouvez obtenir cela en construisant au-dessus de weak_ptr et shared_ptr . Ces classes sont génériques, donc si vous voulez interdire l'utilisation de shared_ptr l'accès à BaseClass alors vous pouvez écrire une spécialisation pour shared_ptr<BaseClass> qui se comporte différemment. Cachez une instance de shared_ptr<BaseClass> pour empêcher la suppression et fournir ces pointeurs par une méthode d'usine sous votre contrôle.

Dans ce modèle, la sémantique de destroy() ont besoin d'attention. Le premier choix est de savoir si vous souhaitez un fonctionnement synchrone ou asynchrone. Un système synchrone destroy() bloquerait jusqu'à ce que tous les pointeurs externes soient libérés et ne permettrait pas l'émission de nouveaux pointeurs. (Je suppose que les constructeurs de copies sont déjà désactivés sur le pointeur). destroy() . La plus simple des deux échoue s'il existe encore des références externes. Appeler unique() sur le caché shared_ptr() rend cette mise en œuvre facile. La plus compliquée agit comme un appel d'E/S asynchrone, en programmant la destruction à un moment donné dans le futur, vraisemblablement dès que toutes les références externes auront disparu. Cette fonction peut être appelée mark_for_death() pour refléter la sémantique, puisque l'objet peut ou non être détruit au moment du retour.

1voto

πάντα ῥεῖ Points 15683

J'envisagerais plutôt d'utiliser l'un des modèles de pointeurs intelligents appropriés. Le comportement de l'accès à un objet supprimé est toujours indéfini et un drapeau "zombie" n'aidera pas vraiment. La mémoire qui était associée à l'instance de l'objet supprimé pourrait être immédiatement occupée par tout autre objet créé, donc l'accès au drapeau zombie n'est pas une information à laquelle vous pouvez faire confiance.

A mon avis, le placement d'un nouvel opérateur

new(this) BaseClass();

utilisé dans votre destroy() ne sera pas vraiment utile. Cela dépend un peu de la façon dont cette méthode est censée être utilisée. Au lieu de supprimer les objets dérivés ou dans les destructeurs des objets supprimés. Dans ce dernier cas, la mémoire sera libérée de toute façon.

UPDATE :

Selon votre édition, ne serait-il pas préférable d'utiliser l'idiome du pointeur partagé/pointeur faible pour résoudre l'occurrence des références circulaires. Sinon, je les considérerais comme un défaut de conception.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X