La suppression est-elle autorisée?

232

Est-il autorisé à delete this;si l'instruction delete est la dernière instruction qui sera exécutée sur cette instance de la classe? Bien sûr, je suis sûr que l'objet représenté par le thispointeur est newcréé de manière ly.

Je pense à quelque chose comme ça:

void SomeModule::doStuff()
{
    // in the controller, "this" object of SomeModule is the "current module"
    // now, if I want to switch over to a new Module, eg:

    controller->setWorkingModule(new OtherModule());

    // since the new "OtherModule" object will take the lead, 
    // I want to get rid of this "SomeModule" object:

    delete this;
}

Puis-je faire ceci?

Martijn Courteaux
la source
13
Le problème principal serait que si vous delete thisavez créé un couplage étroit entre la classe et la méthode d'allocation utilisée pour créer des objets de cette classe. C'est une conception OO très médiocre, car la chose la plus fondamentale dans la POO est de créer des classes autonomes qui ne savent pas ce que leur appelant fait ou se soucient de lui. Ainsi, une classe correctement conçue ne devrait pas savoir ou se soucier de la façon dont elle a été allouée. Si, pour une raison quelconque, vous avez besoin d'un mécanisme aussi particulier, je pense qu'une meilleure conception serait d'utiliser une classe wrapper autour de la classe réelle et de laisser le wrapper gérer l'allocation.
Lundin
Vous ne pouvez pas supprimer setWorkingModule?
Jimmy T.

Réponses:

238

La FAQ C ++ Lite a une entrée spécialement pour cela

Je pense que cette citation résume bien

Tant que vous faites attention, il est normal qu'un objet se suicide (supprimez-le).

JaredPar
la source
15
La FQA correspondante a également quelques commentaires utiles: yosefk.com/c++fqa/heap.html#fqa-16.15
Alexandre C.
1
Pour des raisons de sécurité, vous pouvez utiliser un destructeur privé sur l'objet d'origine pour vous assurer qu'il n'est pas construit sur la pile ou en tant que partie d'un tableau ou d'un vecteur.
Cem Kalyoncu
Définissez «prudent»
CinCout
3
«Attention» est défini dans l'article FAQ lié. (Alors que le lien FQA déchire principalement - comme presque tout ce qu'il
contient
88

Oui, delete this;a des résultats définis, tant que (comme vous l'avez noté) vous vous assurez que l'objet a été alloué dynamiquement, et (bien sûr) ne tentez jamais d'utiliser l'objet après sa destruction. Au fil des ans, de nombreuses questions ont été posées sur le contenu de la norme delete this;, par opposition à la suppression d'un autre pointeur. La réponse est assez courte et simple: elle ne dit pas grand-chose. Il dit simplement que deletel'opérande de doit être une expression qui désigne un pointeur sur un objet ou un tableau d'objets. Il entre dans un peu de détails sur des choses comme la façon dont il détermine quelle fonction de désallocation (le cas échéant) appeler pour libérer la mémoire, mais la section entière sur delete(§ [expr.delete]) ne mentionne pas du tout delete this;spécifiquement. La section sur les destructeurs mentionnedelete this en un seul endroit (§ [class.dtor] / 13):

Au point de définition d'un destructeur virtuel (y compris une définition implicite (15.8)), la fonction de désallocation non-tableau est déterminée comme si pour l'expression supprimer cela apparaissant dans un destructeur non virtuel de la classe du destructeur (voir 8.3.5 ).

Cela tend à soutenir l'idée que la norme considère delete this;comme valide - si elle n'était pas valide, son type ne serait pas significatif. C'est le seul endroit que la norme mentionne delete this;, à ma connaissance.

Quoi qu'il en soit, certains considèrent delete thisun hack méchant et disent à quiconque écoutera qu'il devrait être évité. Un problème fréquemment cité est la difficulté de garantir que les objets de la classe ne sont jamais alloués dynamiquement. D'autres le considèrent comme un idiome parfaitement raisonnable et l'utilisent tout le temps. Personnellement, je suis quelque part au milieu: je l'utilise rarement, mais n'hésitez pas à le faire quand il semble être le bon outil pour le travail.

La première fois que vous utilisez cette technique, c'est avec un objet qui a une vie qui lui est presque entièrement propre. Un exemple que James Kanze a cité est un système de facturation / suivi sur lequel il a travaillé pour une compagnie de téléphone. Lorsque vous commencez à téléphoner, quelque chose en prend note et crée un phone_callobjet. À partir de ce moment, l' phone_callobjet gère les détails de l'appel téléphonique (établir une connexion lorsque vous composez, ajouter une entrée à la base de données pour dire quand l'appel a commencé, connecter éventuellement plus de personnes si vous effectuez une conférence téléphonique, etc.) les dernières personnes ayant raccroché, l' phone_callobjet fait sa comptabilité finale (par exemple, ajoute une entrée à la base de données pour dire quand vous avez raccroché, afin qu'ils puissent calculer la durée de votre appel), puis se détruit. La durée de vie duphone_calll'objet est basé sur le moment où la première personne commence l'appel et lorsque les dernières personnes le quittent - du point de vue du reste du système, il est fondamentalement entièrement arbitraire, vous ne pouvez donc le lier à aucune portée lexicale dans le code , ou quoi que ce soit sur cet ordre.

Pour tous ceux qui pourraient se soucier de la fiabilité de ce type de codage: si vous passez un appel téléphonique vers, depuis ou à travers presque n'importe quelle partie de l'Europe, il y a de fortes chances qu'il soit géré (au moins en partie) par code cela fait exactement cela.

Jerry Coffin
la source
2
Merci, je vais le mettre quelque part dans ma mémoire. Je suppose que vous définissez les constructeurs et destructeurs comme privés et utilisez une méthode d'usine statique pour créer de tels objets.
Alexandre C.
@Alexandre: Vous feriez probablement cela dans la plupart des cas de toute façon - je ne connais pas tous les détails du système sur lequel il travaillait, donc je ne peux pas en être sûr.
Jerry Coffin
La façon dont je contourne souvent le problème de la façon dont la mémoire a été allouée est d'inclure un bool selfDeleteparamètre dans le constructeur qui est attribué à une variable membre. Certes, cela signifie remettre au programmeur suffisamment de corde pour y attacher un nœud coulant, mais je trouve cela préférable aux fuites de mémoire.
MBraedley
1
@MBraedley: J'ai fait de même, mais je préfère éviter ce qui me semble être un coup de coude.
Jerry Coffin du
Pour tous ceux qui pourraient s'en soucier ... il y a de fortes chances qu'il soit géré (au moins en partie) par un code qui le fait exactement this. Oui, le code est géré par exactement this. ;)
Galaxy
46

Si cela vous fait peur, il y a un hack parfaitement légal:

void myclass::delete_me()
{
    std::unique_ptr<myclass> bye_bye(this);
}

Je pense que delete thisc'est du C ++ idiomatique, et je ne présente cela que comme une curiosité.

Il existe un cas où cette construction est réellement utile - vous pouvez supprimer l'objet après avoir levé une exception qui a besoin des données de membre de l'objet. L'objet reste valide jusqu'à ce que le lancer ait lieu.

void myclass::throw_error()
{
    std::unique_ptr<myclass> bye_bye(this);
    throw std::runtime_exception(this->error_msg);
}

Remarque: si vous utilisez un compilateur plus ancien que C ++ 11 que vous pouvez utiliser à la std::auto_ptrplace de std::unique_ptr, il fera la même chose.

Mark Ransom
la source
Je n'arrive pas à compiler ceci en utilisant c ++ 11, y a-t-il des options de compilation spéciales pour cela? Ne nécessite-t-il pas non plus un déplacement du pointeur this?
Hibou
@Owl ne sais pas ce que vous voulez dire, cela fonctionne pour moi: ideone.com/aavQUK . La création d'un à unique_ptrpartir d' un autre unique_ptr nécessite un déplacement, mais pas à partir d'un pointeur brut. A moins que les choses ne changent en C ++ 17?
Mark Ransom
Ahh C ++ 14, ce sera pourquoi. J'ai besoin de mettre à jour mon c ++ sur ma boîte de développement. Je vais réessayer ce soir sur mon système Gentoo récemment sorti!
Chouette
25

L'une des raisons pour lesquelles C ++ a été conçu était de faciliter la réutilisation du code. En général, C ++ doit être écrit pour qu'il fonctionne, que la classe soit instanciée sur le tas, dans un tableau ou sur la pile. "Supprimer ceci" est une très mauvaise pratique de codage car cela ne fonctionnera que si une seule instance est définie sur le tas; et il vaut mieux ne pas avoir une autre instruction delete, qui est généralement utilisée par la plupart des développeurs pour nettoyer le tas. Cela suppose également qu'aucun programmeur de maintenance à l'avenir ne guérira une fuite de mémoire faussement perçue en ajoutant une instruction de suppression.

Même si vous savez à l'avance que votre plan actuel est de n'allouer qu'une seule instance sur le tas, que se passe-t-il si un développeur heureux arrive à l'avenir et décide de créer une instance sur la pile? Ou, que se passe-t-il s'il coupe et colle certaines parties de la classe dans une nouvelle classe qu'il a l'intention d'utiliser sur la pile? Lorsque le code atteint "supprimer ceci", il s'éteint et le supprime, mais lorsque l'objet sort de la portée, il appelle le destructeur. Le destructeur tentera alors de le supprimer à nouveau et vous serez arrosé. Dans le passé, faire quelque chose comme ça reviendrait à foutre non seulement le programme mais aussi le système d'exploitation et l'ordinateur à redémarrer. Dans tous les cas, cela est fortement déconseillé et devrait presque toujours être évité. Je devrais être désespéré, sérieusement enduit,

Bob Bryan
la source
7
+1. Je ne comprends pas pourquoi vous avez été rétrogradé. "C ++ doit être écrit pour qu'il fonctionne, que la classe soit instanciée sur le tas, dans un tableau ou sur la pile" est un très bon conseil.
Joh
1
Vous pouvez simplement envelopper l'objet que vous souhaitez supprimer lui-même dans une classe spéciale qui supprime l'objet, puis lui-même, et utiliser cette technique pour empêcher l'allocation de pile: stackoverflow.com/questions/124880/… Il y a des moments où il n'y a vraiment pas de alternative viable. Je viens d'utiliser cette technique pour supprimer automatiquement un thread qui est démarré par une fonction DLL, mais la fonction DLL doit retourner avant la fin du thread.
Felix Dombek
Vous ne pouvez pas programmer de telle manière que quelqu'un qui fait simplement du copier-coller avec votre code finisse par en abuser de toute façon
Jimmy T.
22

Il est autorisé (n'utilisez pas l'objet après cela), mais je n'écrirais pas un tel code sur la pratique. Je pense que delete thisdevrait apparaître que dans les fonctions qui a appelé releaseou Releaseet ressemble à : void release() { ref--; if (ref<1) delete this; }.

Kirill V. Lyadvinsky
la source
Ce qui est exactement une fois dans chacun de mes projets ... :-)
cmaster - réintègre monica
15

Eh bien, dans la delete thisconstruction COM (Component Object Model) peut être une partie de la Releaseméthode qui est appelée chaque fois que vous souhaitez libérer un objet acquis:

void IMyInterface::Release()
{
    --instanceCount;
    if(instanceCount == 0)
        delete this;
}
UnknownGosu
la source
8

Il s'agit de l'idiome de base pour les objets comptés par référence.

Le comptage de références est une forme forte de collecte de déchets déterministe - il garantit que les objets gèrent leur propre durée de vie au lieu de compter sur des pointeurs «intelligents», etc. pour le faire pour eux. L'objet sous-jacent n'est accessible que via des pointeurs intelligents "Référence", conçus pour que les pointeurs incrémentent et décrémentent un entier membre (le nombre de références) dans l'objet réel.

Lorsque la dernière référence tombe de la pile ou est supprimée, le nombre de références passe à zéro. Le comportement par défaut de votre objet sera alors un appel à "supprimer ceci" pour le ramasse-miettes - les bibliothèques que j'écris fournissent un appel virtuel "CountIsZero" protégé dans la classe de base afin que vous puissiez remplacer ce comportement pour des choses comme la mise en cache.

La clé pour rendre ce coffre-fort n'est pas de permettre aux utilisateurs d'accéder au CONSTRUCTEUR de l'objet en question (le rendre protégé), mais plutôt de les faire appeler un membre statique - le "FACTORY" comme "Reference Reference CreateT (...)". De cette façon, vous SAVEZ avec certitude qu'ils sont toujours construits avec un "nouveau" ordinaire et qu'aucun pointeur brut n'est jamais disponible, donc "supprimer ceci" ne explosera jamais.

Zack Yezek
la source
Pourquoi ne pouvez-vous pas simplement avoir une classe (singleton) "allocator / garbage collector", une interface à travers laquelle toutes les allocations sont effectuées et laisser cette classe gérer tous les comptages de références des objets alloués? Plutôt que de forcer les objets eux-mêmes à s'embêter avec des tâches de collecte des ordures, quelque chose qui n'a aucun rapport avec leur objectif.
Lundin
1
Vous pouvez également simplement protéger le destructeur pour interdire les allocations statiques et de pile de votre objet.
Game_Overture
7

Tu peux le faire. Cependant, vous ne pouvez pas l'affecter. Ainsi, la raison pour laquelle vous déclarez faire cela, "je veux changer la vue", semble très discutable. La meilleure méthode, à mon avis, serait que l'objet qui contient la vue remplace cette vue.

Bien sûr, vous utilisez des objets RAII et vous n'avez donc pas du tout besoin d'appeler supprimer ... n'est-ce pas?

Edward Strange
la source
4

C'est une vieille question à laquelle il a répondu, mais @Alexandre a demandé "Pourquoi quelqu'un voudrait-il faire cela?", Et j'ai pensé que je pourrais fournir un exemple d'utilisation que j'envisage cet après-midi.

Code hérité. Utilise des pointeurs nus Obj * obj avec un obj de suppression à la fin.

Malheureusement, j'ai besoin parfois, pas souvent, de garder l'objet en vie plus longtemps.

J'envisage d'en faire un pointeur intelligent compté par référence. Mais il y aurait beaucoup de code à changer si je devais l'utiliser ref_cnt_ptr<Obj>partout. Et si vous mélangez Obj * et ref_cnt_ptr nus, vous pouvez supprimer l'objet implicitement lorsque le dernier ref_cnt_ptr disparaît, même si Obj * est toujours vivant.

Je pense donc à créer un explicit_delete_ref_cnt_ptr. C'est-à-dire un pointeur compté par référence où la suppression n'est effectuée que dans une routine de suppression explicite. En l'utilisant au seul endroit où le code existant connaît la durée de vie de l'objet, ainsi que dans mon nouveau code qui maintient l'objet en vie plus longtemps.

L'incrémentation et la décrémentation du nombre de références lorsque explicit_delete_ref_cnt_ptr sont manipulées.

Mais PAS de libération lorsque le nombre de références est considéré comme nul dans le destructeur explicit_delete_ref_cnt_ptr.

Libération uniquement lorsque le nombre de références est considéré comme nul dans une opération de type suppression explicite. Par exemple dans quelque chose comme:

template<typename T> class explicit_delete_ref_cnt_ptr { 
 private: 
   T* ptr;
   int rc;
   ...
 public: 
   void delete_if_rc0() {
      if( this->ptr ) {
        this->rc--;
        if( this->rc == 0 ) {
           delete this->ptr;
        }
        this->ptr = 0;
      }
    }
 };

OK, quelque chose comme ça. Il est un peu inhabituel qu'un type de pointeur compté par référence ne supprime pas automatiquement l'objet pointé dans le destructeur ptr rc'ed. Mais il semble que cela puisse rendre un peu plus sûr le mélange des pointeurs nus et des pointeurs rc'ed.

Mais jusqu'à présent, pas besoin de supprimer cela.

Mais ensuite, cela m'est venu à l'esprit: si l'objet pointé, la pointee, sait qu'il est compté par référence, par exemple si le compte est à l'intérieur de l'objet (ou dans une autre table), alors la routine delete_if_rc0 pourrait être une méthode de la objet pointé, pas le pointeur (intelligent).

class Pointee { 
 private: 
   int rc;
   ...
 public: 
   void delete_if_rc0() {
        this->rc--;
        if( this->rc == 0 ) {
           delete this;
        }
      }
    }
 };

En fait, il ne doit pas du tout être une méthode membre, mais peut être une fonction libre:

map<void*,int> keepalive_map;
template<typename T>
void delete_if_rc0(T*ptr) {
        void* tptr = (void*)ptr;
        if( keepalive_map[tptr] == 1 ) {
           delete ptr;
        }
};

(BTW, je sais que le code n'est pas tout à fait correct - il devient moins lisible si j'ajoute tous les détails, donc je le laisse comme ça.)

Krazy Glew
la source
0

Supprimer ceci est légal tant que l'objet est en tas. Vous auriez besoin d'exiger que l'objet soit un tas uniquement. La seule façon de le faire est de protéger le destructeur - de cette façon, delete peut être appelé UNIQUEMENT à partir de la classe, donc vous auriez besoin d'une méthode qui garantirait la suppression

Swift - Friday Pie
la source