Qui supprime la mémoire allouée lors d’une «nouvelle» opération qui a une exception dans le constructeur?

Je ne peux vraiment pas croire que je n’ai pas trouvé de réponse claire à cette question …

Comment libérez-vous la mémoire allouée après qu’un constructeur de classe C ++ lève une exception, dans le cas où il est initialisé à l’aide de l’opérateur new . Par exemple:

 class Blah { public: Blah() { throw "oops"; } }; void main() { Blah* b = NULL; try { b = new Blah(); } catch (...) { // What now? } } 

Lorsque j’ai essayé ceci, b est NULL dans le bloc catch (ce qui est logique).

Lors du débogage, j’ai remarqué que la commande entre dans la routine d’allocation de mémoire AVANT qu’elle n’atteigne le constructeur.

Ceci sur le site MSDN semble confirmer ceci :

Lorsque new est utilisé pour allouer de la mémoire pour un object de classe C ++, le constructeur de l’object est appelé après l’allocation de la mémoire.

Donc, en gardant à l’esprit que la variable locale b n’est jamais affectée (c’est-à-dire qu’elle est NULL dans le bloc catch), comment supprimez-vous la mémoire allouée?

Il serait également intéressant d’obtenir une réponse multi-plateforme à ce sujet. c’est à dire, que dit la spécification C ++?

CLARIFICATION: Je ne parle pas du cas où la classe a alloué la mémoire elle-même dans le c’tor puis jette. J’apprécie le fait que dans ces cas, l’tor ne sera pas appelé. Je parle de la mémoire utilisée pour allouer l’object ( Blah dans mon cas).

Vous devriez vous référer aux questions similaires ici et ici . Fondamentalement, si le constructeur lance une exception, vous êtes sûr que la mémoire de l’object est libérée à nouveau. Bien que si une autre mémoire a été revendiquée pendant le constructeur, vous êtes libre de la libérer avant de laisser le constructeur avec l’exception.

Pour votre question QUI supprime la mémoire, la réponse est le code derrière le nouvel opérateur (qui est généré par le compilateur). S’il reconnaît une exception quittant le constructeur, il doit appeler tous les destructeurs des classes membres (comme ceux qui ont déjà été construits avec succès avant d’appeler le code constructeur) et libérer leur mémoire (pourrait être récursive avec l’appel destructeur, probablement en appelant une suppression appropriée sur eux) ainsi que libérer la mémoire allouée pour cette classe elle-même. Ensuite, il doit redirect l’exception capturée du constructeur vers l’appelant de new . Bien sûr, il y a peut-être encore du travail à faire mais je ne peux pas tirer tous les détails de mon esprit car ils sont à la hauteur de l’implémentation de chaque compilateur.

Si un object ne peut pas terminer sa destruction parce que le constructeur lève une exception, la première chose à faire (dans le cadre du traitement spécial du constructeur) est la destruction de toutes les variables membres – si une exception est générée dans la liste d’initialisation , cela signifie que seuls les éléments pour lesquels l’initialiseur s’est terminé sont détruits.

Ensuite, si l’object était alloué avec new , la fonction de libération ( operator delete ) appropriée est appelée avec les mêmes arguments supplémentaires qui ont été transmis à operator new . Par exemple, new (std::nothrow) SomethingThatThrows() allouera de la mémoire avec l’ operator new (size_of_ob, nothrow) , tentera de construire SomethingThatThrows , détruira tous les membres qui ont été construits avec succès, puis appellera operator delete (ptr_to_obj, nothrow) se propage – il n’y aura pas de fuite de mémoire.

Ce que vous devez faire attention est d’allouer plusieurs objects en succession – si l’un des derniers lancés, les précédents ne seront pas automatiquement désalloués. Le meilleur moyen de contourner ce problème est d’utiliser des pointeurs intelligents, car en tant qu’objects locaux, leurs destructeurs seront appelés lors du déroulement de la stack et leurs destructeurs désalloueront correctement la mémoire.

A partir de la norme C ++ 2003 5.3.4 / 17 – Nouveau:

Si une partie de l’initialisation d’object décrite ci-dessus se termine en lançant une exception et qu’une fonction de désallocation appropriée peut être trouvée, la fonction deallocation est appelée pour libérer la mémoire dans laquelle l’object était construit, après quoi l’exception continue à se propager dans le contexte. de la nouvelle expression. Si aucune fonction de désallocation sans ambiguïté ne peut être trouvée, la propagation de l’exception ne libère pas la mémoire de l’object. [Remarque: Ceci est approprié lorsque la fonction d’allocation appelée n’alloue pas de mémoire; sinon, cela risque de provoquer une fuite de mémoire. ]

Il peut donc y avoir ou non une fuite – cela dépend si un désallocateur approprié peut être trouvé (ce qui est normalement le cas, sauf si l’opérateur new / delete a été remplacé). Dans le cas où il y a un désassociateur approprié, le compilateur est responsable pour le câblage dans un appel si le constructeur jette.

Notez que ceci est plus ou moins sans rapport avec ce qui arrive aux ressources acquises dans le constructeur, ce que ma première tentative de réponse a discuté – et est une question qui est discutée dans de nombreuses FAQ, articles et publications.

Si le constructeur lève la mémoire allouée, l’object est renvoyé comme par magie au système.

Notez que le destructeur de la classe qui a jeté ne sera pas appelé.
Mais le destructeur de toute classe de base (où le constructeur de base est terminé) sera également appelé.

Remarque:
Comme la plupart des gens l’ont remarqué, les membres peuvent avoir besoin de nettoyage.

Les destructeurs des membres qui ont été complètement initialisés seront appelés, mais si vous possédez des membres de pointeur RAW (par exemple, supprimez-les dans le destructeur), vous devrez effectuer un nettoyage avant de lancer (une autre raison de ne pas utiliser Des pointeurs RAW dans votre classe).

 #include  class Base { public: Base() {std::cout << "Create Base\n";} ~Base() {std::cout << "Destroy Base\n";} }; class Deriv: public Base { public: Deriv(int x) {std::cout << "Create Deriv\n";if (x > 0) throw int(x);} ~Deriv() {std::cout << "Destroy Deriv\n";} }; int main() { try { { Deriv d0(0); // All constructors/Destructors called. } { Deriv d1(1); // Base constructor and destructor called. // Derived constructor called (not destructor) } } catch(...) { throw; // Also note here. // If an exception escapes main it is implementation defined // whether the stack is unwound. By catching in main() you force // the stack to unwind to this point. If you can't handle re-throw // so the system exception handling can provide the appropriate // error handling (such as user messages). } } 

En bref, si vous n’avez pas effectué d’allocation d’autres entités dans votre object (comme dans votre exemple), la mémoire allouée sera automatiquement supprimée. Cependant, toute nouvelle instruction (ou tout autre élément gérant directement la mémoire) doit être gérée dans une instruction catch dans le constructeur. Sinon, l’object est supprimé sans supprimer ses allocations ultérieures et vous, mon ami, avez une fuite.

Cité à partir de la FAQ C ++ ( parashift.com ):

[17.4] Comment dois-je gérer les ressources si mes constructeurs peuvent générer des exceptions?

Chaque membre de données à l’intérieur de votre object devrait nettoyer son propre désordre.

Si un constructeur lève une exception, le destructeur de l’object n’est pas exécuté. Si votre object a déjà fait quelque chose qui doit être annulé (par exemple, allouer de la mémoire, ouvrir un fichier ou verrouiller un sémaphore), un membre de données à l’intérieur de l’object doit mémoriser ce “truc qui doit être annulé”.

Par exemple, plutôt que d’allouer de la mémoire à un membre de données Fred* brut, placez la mémoire allouée dans un object membre «pointeur intelligent» et le destructeur de ce pointeur intelligent delete l’object Fred lorsque le pointeur intelligent meurt. Le modèle std::auto_ptr est un exemple de type “pointeur intelligent”. Vous pouvez également écrire votre propre pointeur intelligent de comptage de référence . Vous pouvez également utiliser des pointeurs intelligents pour “pointer” vers des enregistrements de disque ou des objects sur d’autres machines .

Au fait, si vous pensez que votre classe Fred va être allouée dans un pointeur intelligent, soyez gentil avec vos utilisateurs et créez un typedef dans votre classe Fred :

  #include  class Fred { public: typedef std::auto_ptr Ptr; ... }; 

Ce typedef simplifie la syntaxe de tout le code qui utilise vos objects: vos utilisateurs peuvent dire Fred::Ptr au lieu de std::auto_ptr :

  #include "Fred.h" void f(std::auto_ptr p); // explicit but verbose void f(Fred::Ptr p); // simpler void g() { std::auto_ptr p1( new Fred() ); // explicit but verbose Fred::Ptr p2( new Fred() ); // simpler ... } 

Le problème décrit est aussi ancien que la route de Rome, pour utiliser un dicton néerlandais. J’ai mis au point le problème et une allocation de mémoire pour un object susceptible de générer une exception se présente comme suit:

 try { std::ssortingng *l_ssortingng = (_heap_cleanup_tpl(&l_ssortingng), new std::ssortingng(0xf0000000, ' ')); delete l_ssortingng; } catch(std::exception &) { } 

Avant l’appel effectif au new opérateur, un object sans nom (temporaire) est créé, qui reçoit l’adresse de la mémoire allouée par l’intermédiaire d’un nouvel opérateur défini par l’utilisateur (voir le rest de cette réponse). En cas d’exécution normale du programme, l’object temporaire transmet le résultat du nouvel opérateur (l’object nouvellement créé et entièrement construit, dans notre cas, une très très longue chaîne) à la variable l_ssortingng . En cas d’exception, la valeur n’est pas transmise, mais le destructeur de l’object temporaire supprime la mémoire (bien sûr sans appeler le destructeur de l’object principal).

C’est une manière un peu floue de traiter le problème, mais ça marche. Des problèmes peuvent survenir parce que cette solution nécessite un nouvel opérateur défini par l’utilisateur et un opérateur de suppression défini par l’utilisateur. Les opérateurs nouveaux / delete-user définis par l’utilisateur devraient appeler l’implémentation de la bibliothèque standard C ++ des opérateurs new / delete, mais j’ai laissé cela de côté et je me suis appuyé sur malloc() et free() .

Ce n’est pas la réponse finale, mais je pense que cela vaut la peine de travailler sur celui-ci.

PS: Le code ci-dessous comportait une fonctionnalité “non documentée”, alors j’ai apporté une amélioration.

Le code de l’object temporaire est le suivant:

 class _heap_cleanup_helper { public: _heap_cleanup_helper(void **p_heap_block) : m_heap_block(p_heap_block), m_previous(m_last), m_guard_block(NULL) { *m_heap_block = NULL; m_last = this; } ~_heap_cleanup_helper() { if (*m_heap_block == NULL) operator delete(m_guard_block); m_last = m_previous; } void **m_heap_block, *m_guard_block; _heap_cleanup_helper *m_previous; static _heap_cleanup_helper *m_last; }; _heap_cleanup_helper *_heap_cleanup_helper::m_last; template  class _heap_cleanup_tpl : public _heap_cleanup_helper { public: _heap_cleanup_tpl(p_alloc_type **p_heap_block) : _heap_cleanup_helper((void **)p_heap_block) { } }; 

Le nouvel opérateur défini par l’utilisateur est le suivant:

 void *operator new (size_t p_cbytes) { void *l_retval = malloc(p_cbytes); if ( l_retval != NULL && *_heap_cleanup_helper::m_last->m_heap_block == NULL && _heap_cleanup_helper::m_last->m_guard_block == NULL ) { _heap_cleanup_helper::m_last->m_guard_block = l_retval; } if (p_cbytes != 0 && l_retval == NULL) throw std::bad_alloc(); return l_retval; } void operator delete(void *p_buffer) { if (p_buffer != NULL) free(p_buffer); } 

Je pense que c’est un peu bizarre pour un constructeur de faire une exception. Pourriez-vous avoir une valeur de retour et la tester dans votre main?

 class Blah { public: Blah() { if Error { this.Error = "oops"; } } }; void main() { Blah* b = NULL; b = new Blah(); if (b.Error == "oops") { delete (b); b = NULL; }