web-dev-qa-db-fra.com

Destructeur C++ avec retour

En C++, si nous définissons un destructeur de classe comme:

~Foo(){
   return;
}

en appelant ce destructeur, l'objet de Foo sera détruit ou bien retournant explicitement du destructeur signifie que nous ne voulons jamais le détruire.

Je veux faire en sorte qu'un certain objet ne soit détruit que par l'intermédiaire d'un autre destructeur d'objets, c'est-à-dire que lorsque l'autre objet est prêt à être détruit.

Exemple:

class Class1{
...
Class2* myClass2;
...
};

Class1::~Class1(){
    myClass2->status = FINISHED;
    delete myClass2;
}

Class2::~Class2(){
    if (status != FINISHED) return;
}

J'ai cherché en ligne et je ne paraisais pas trouver de réponse à ma question ..__J'ai aussi essayé de le découvrir moi-même en parcourant du code étape par étape avec un débogueur mais je ne peux pas obtenir de résultat concluant.

39
Rasula

Non, vous ne pouvez pas empêcher la destruction de l'objet par l'instruction return, cela signifie simplement que l'exécution du corps du dtor se terminera à ce point. Après cela, il sera toujours détruit (y compris ses membres et ses bases) et la mémoire sera toujours désallouée.

Vous lancez une exception.

Class2::~Class2() noexcept(false) {
    if (status != FINISHED) throw some_exception();
}

Class1::~Class1() {
    myClass2->status = FINISHED;
    try {
        delete myClass2;
    } catch (some_exception& e) {
        // what should we do now?
    }
}

Notez que c'est une idée terrible en effet. Vous feriez mieux de revoir le design, je suis sûr qu'il doit y en avoir un meilleur.

MODIFIER

J'ai commis une erreur: une exception en jetant n'arrête pas la destruction de ses bases et de ses membres, mais permet simplement d'obtenir le résultat du processus du décompte de Class2. Et ce qui pourrait être fait avec cela n'est toujours pas clair.

45
songyuanyao
~Foo(){
   return;
}

signifie exactement la même chose que:

~Foo() {}

Cela ressemble à une fonction void; Atteindre la fin sans une instruction return; équivaut à avoir return; à la fin.

Le destructeur contient des actions qui sont exécutées lorsque le processus de destruction de Foo a déjà commencé. Il n'est pas possible d'interrompre un processus de destruction sans abandonner tout le programme.

24
M.M

[D] oes revenant explicitement du destructeur signifie-t-il que nous ne voulons jamais le détruire?

Non. Un retour anticipé (via return; ou throw ...) signifie uniquement que le reste du corps du destructeur n'est pas exécuté. La base et les membres sont toujours détruits et leurs destructeurs continuent de fonctionner, voir [except.ctor]/3 .

Pour un objet de type classe de toute durée de stockage dont l'initialisation ou la destruction est terminée par une exception, le destructeur est appelé pour chacun des sous-objets entièrement construits de l'objet ...

Voir ci-dessous pour des exemples de code de ce comportement.


Je veux faire en sorte qu'un certain objet ne soit détruit que par l'intermédiaire d'un autre destructeur d'objets, c'est-à-dire que lorsque l'autre objet est prêt à être détruit.

On dirait que la question est enracinée dans la question de la propriété. Suppression de l'objet "possédé" uniquement lorsque le parent est détruit dans un langage très courant et obtenu avec l'un des éléments suivants (sans toutefois s'y limiter);

  • Composition, c’est une variable de membre automatique (c'est-à-dire "basée sur la pile")
  • Un std::unique_ptr<> pour exprimer la propriété exclusive de l'objet dynamique
  • Un std::shared_ptr<> pour exprimer la propriété partagée d'un objet dynamique

Étant donné l'exemple de code dans l'OP, le std::unique_ptr<> peut être une alternative appropriée;

class Class1 {
  // ...
  std::unique_ptr<Class2> myClass2;
  // ...
};

Class1::~Class1() {
    myClass2->status = FINISHED;
    // do not delete, the deleter will run automatically
    // delete myClass2;
}

Class2::~Class2() {
    //if (status != FINISHED)
    //  return;
    // We are finished, we are being deleted.
}

Je note la vérification de la condition if dans l'exemple de code. Cela suggère que l'État est lié à la propriété et à la durée de vie. Ils ne sont pas tous la même chose; Bien sûr, vous pouvez lier l’objet atteignant un certain état à sa durée de vie "logique" (c’est-à-dire exécuter du code de nettoyage), mais j’éviterais le lien direct avec la propriété de l’objet. Il serait peut-être préférable de reconsidérer une partie de la sémantique en jeu ici ou de permettre à la construction et à la destruction "naturelles" de dicter le début et la fin de l'objet.

Note latérale; si vous devez rechercher un état dans le destructeur (ou affirmer une condition de fin), une alternative à la throw consiste à appeler std::terminate (avec une certaine journalisation) si cette condition n'est pas remplie. Cette approche est similaire au comportement standard et au résultat lorsqu'une exception est générée lors du déroulement de la pile à la suite d'une exception déjà générée. C'est également le comportement standard lorsqu'un std::thread se ferme avec une exception non gérée.


[D] oes revenant explicitement du destructeur signifie-t-il que nous ne voulons jamais le détruire?

Non (voir ci-dessus). Le code suivant illustre ce comportement. lié ici et une version dynamique . La noexcept(false) est nécessaire pour éviter que std::terminate() ne s'appelle.

#include <iostream>
using namespace std;
struct NoisyBase {
    NoisyBase() { cout << __func__ << endl; }
    ~NoisyBase() { cout << __func__ << endl; }
    NoisyBase(NoisyBase const&) { cout << __func__ << endl; }
    NoisyBase& operator=(NoisyBase const&) { cout << __func__ << endl; return *this; }    
};
struct NoisyMember {
    NoisyMember() { cout << __func__ << endl; }
    ~NoisyMember() { cout << __func__ << endl; }
    NoisyMember(NoisyMember const&) { cout << __func__ << endl; }
    NoisyMember& operator=(NoisyMember const&) { cout << __func__ << endl; return *this; }    
};
struct Thrower : NoisyBase {
    Thrower() { cout << __func__ << std::endl; }
    ~Thrower () noexcept(false) {
        cout << "before throw" << endl;
        throw 42;
        cout << "after throw" << endl;
    }
    NoisyMember m_;
};
struct Returner : NoisyBase {
    Returner() { cout << __func__ << std::endl; }
    ~Returner () noexcept(false) {
        cout << "before return" << endl;
        return;
        cout << "after return" << endl;
    }
    NoisyMember m_;
};
int main()
{
    try {
        Thrower t;
    }
    catch (int& e) {
        cout << "catch... " << e << endl;
    }
    {
        Returner r;
    }
}

A la sortie suivante;

NoisyBase
NoisyMember
Thrower
before throw
~NoisyMember
~NoisyBase
catch... 42
NoisyBase
NoisyMember
Returner
before return
~NoisyMember
~NoisyBase
17
Niall

Selon la norme C++ (12.4 destructeurs)

8 Après avoir exécuté le corps du destructeur et en avoir détruit un objets automatiques alloués dans le corps, destructeur de classe X appelle les destructeurs pour les données non statiques directes non variantes de X membres, les destructeurs des classes de base directes de X et, si X est le type de la classe la plus dérivée (12.6.2), son destructeur appelle le destructeurs pour les classes de base virtuelles de X. Tous les destructeurs s'appellent comme s'ils étaient référencés avec un nom qualifié, c'est-à-dire en ignorant tous les destructeurs de substitution virtuels possibles dans des classes plus dérivées . Les bases et les membres sont détruits dans l'ordre inverse de l'achèvement des travaux de leur constructeur (voir 12.6.2). Une déclaration de retour (6.6.3) en a le destructeur peut ne pas retourner directement à l'appelant; avant transférer le contrôle à l'appelant, les destructeurs pour les membres et les bases sont appelées. Les destructeurs des éléments d'un tableau sont appelés dans l'ordre inverse de leur construction (voir 12.6).

Ainsi, une instruction return n'empêche pas l'objet pour lequel le destructeur est appelé à être détruit.

8
Vlad from Moscow

le fait de revenir explicitement du destructeur signifie-t-il que nous ne voulons jamais le détruire.

Non. 

Le destructeur est une fonction. Vous pouvez donc utiliser le mot clé return à l'intérieur, mais cela n'empêchera pas la destruction de l'objet. Une fois que vous êtes à l'intérieur du destructeur, vous détruisez déjà votre objet. se produire avant.

Pour une raison quelconque, je pense intuitivement que votre problème de conception peut être résolu avec un shared_ptr et peut-être un suppresseur personnalisé, mais cela nécessiterait plus d’informations sur ledit problème.

7
Drax

Donc, comme tous les autres l'ont fait remarquer, return n'est pas une solution. 

La première chose que je voudrais ajouter est que vous ne devriez généralement pas vous inquiéter à ce sujet. À moins que votre professeur ne le demande explicitement. Il serait très étrange que vous ne puissiez pas faire confiance à la classe externe pour ne supprimer que votre classe au bon moment, et je suppose que personne d'autre ne la voit. Si le pointeur est passé autour, il serait très probablement shared_ptr/weak_ptr, et le laisserait détruire votre classe au bon moment. 

Mais, hé, il est bon de se demander comment nous pourrions résoudre un problème étrange s'il se posait, si nous apprenions quelque chose (et ne perdons pas de temps en respectant les délais!)

Alors quoi pour une solution? Si vous pouvez au moins faire confiance au destructeur de Class1 pour ne pas détruire votre objet trop tôt, vous pouvez simplement déclarer le destructeur de Class2 comme privé, puis déclarer le destructeur de Class1 en tant qu'ami de Class2, comme suit:

class Class2;

class Class1 {

    Class2* myClass2;

public:
    ~Class1();
};

class Class2 {
private:
        ~Class2();

friend Class1::~Class1();
};

Class1::~Class1() {
    delete myClass2;
}

Class2::~Class2() {
}

En prime, vous n'avez pas besoin du drapeau 'statut'; ce qui est bon - si quelqu'un voulait si bien vous baiser, pourquoi ne pas définir l'indicateur de statut sur FINISHED ailleurs, puis appeler delete

De cette façon, vous avez la garantie réelle que l'objet ne peut être détruit que dans le destructeur de Class1. 

Bien entendu, le destructeur de Class1 a accès à tous les membres privés de Class2. Peu importe, après tout, la classe 2 est sur le point d'être détruite de toute façon! Mais si tel est le cas, nous pouvons évoquer des moyens encore plus compliqués de le contourner. pourquoi pas. Par exemple:

class Class2;

class Class1 {
private:
    int status;

    Class2* myClass2;

public:
    ~Class1();
};

class Class2Hidden {
private:
      //Class2 private members
protected:
    ~Class2Hidden();
public:
    //Class2 public members
};

class Class2 : public Class2Hidden {
protected:
        ~Class2();

friend Class1::~Class1();
};

Class1::~Class1() {
    delete myClass2;
}

Class2Hidden::~Class2Hidden() {
}

Class2::~Class2() {
}

De cette façon, les membres publics seront toujours disponibles dans la classe dérivée, mais les membres privés seront réellement privés. ~ Class1 n'aura accès qu'aux membres privés et protégés de Class2 et aux membres protégés de Class2Hidden; qui dans ce cas n'est que les destructeurs. Si vous devez protéger un membre protégé de Class2 contre le destructeur de Class1 ... il existe des moyens, mais cela dépend vraiment de ce que vous faites.

Bonne chance!

1
Francesco Dondi

Bien sûr que non. L'appel explicite de 'return' est 100% équivalent à un retour implicite après l'exécution du destructeur.

1
Trantor

Vous pouvez créer une nouvelle méthode pour que l'objet "se suicide" et que le destructeur soit vide, afin que quelque chose comme ceci fasse le travail que vous souhaitez:

Class1::~Class1()
{
    myClass2->status = FINISHED;
    myClass2->DeleteMe();
}

void Class2::DeleteMe()
{
   if (status == FINISHED)
   {
      delete this;
   }
 }

 Class2::~Class2()
 {
 }
1
Hasson

Tous les objets basés sur des piles à l'intérieur seront détruits, peu importe le temps que vous avez passé à return à partir du destructeur. Si vous manquez à delete objets alloués dynamiquement, il y aurait une fuite de mémoire intentionnelle

C'est toute l'idée du fonctionnement de move-constructors. Le mouvement CTOR prendrait simplement la mémoire de l'objet original. Le destructeur de l’objet original n’appelle tout simplement pas/ne peut pas appeler delete.

1
Ajay

return signifie simplement quitter la méthode, cela n'arrête pas la destruction de l'objet. 

Aussi, pourquoi voudriez-vous? Si l'objet est alloué sur la pile et que vous avez réussi à arrêter la destruction, il vivrait sur une partie de la pile récupérée qui sera probablement remplacée par le prochain appel de fonction, ce qui pourrait écrire dans la mémoire de l'objet et créer un comportement indéfini .

De même, si l'objet est alloué sur le tas et que vous réussissez à empêcher la destruction, vous aurez une fuite de mémoire car le code appelant delete supposerait qu'il n'a pas besoin de conserver un pointeur sur l'objet alors qu'il est toujours là. et en prenant la mémoire que personne ne fait référence.

1
Sean

Dans ce cas, vous pouvez utiliser une surcharge spécifique à la classe de l'opérateur de suppression .

class Class2
{
    static void operator delete(void* ptr, std::size_t sz)
    {
        std::cout << "custom delete for size " << sz << '\n';
        if(finished)
            ::operator delete(ptr);
    }

    bool Finished;
}

Ensuite, si vous définissez la valeur de l'état done avant la suppression, la suppression réelle sera appelée . Notez que je ne l'ai pas testée, je viens de modifier le code que j'ai trouvé ici http://en.cppreference.com/w/cpp/memory/new/operator_delete

Class1::~Class1()
{
    class2->Finished = true;
    delete class2;
}
0
Stamatis Liatsos