webentwicklung-frage-antwort-db.com.de

C++ - Destruktor mit Rückgabe

Wenn wir in C++ einen Klassendestruktor definieren:

~Foo(){
   return;
}

wenn dieser Destruktor aufgerufen wird, wird das Objekt von Foo zerstört werden oder bedeutet, dass das explizite Zurückkehren vom Destruktor bedeutet, dass wir ihn niemals zerstören wollen.

Ich möchte es so machen, dass ein bestimmtes Objekt nur durch einen Zerstörer anderer Objekte zerstört wird, d. H. Nur wenn das andere Objekt bereit ist, zerstört zu werden.

Beispiel:

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

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

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

Ich habe online gesucht und konnte anscheinend keine Antwort auf meine Frage finden ... Ich habe es auch selbst ausprobiert, indem ich mit einem Debugger Schritt für Schritt Code durchgelaufen habe, aber kein abschließendes Ergebnis erhalten kann.

39
Rasula

Nein, Sie können nicht verhindern, dass das Objekt durch eine return-Anweisung zerstört wird. Es bedeutet nur, dass die Ausführung des Körpers des Dtors an diesem Punkt endet. Danach wird es immer noch zerstört (einschließlich seiner Mitglieder und Basen), und die Erinnerung wird weiterhin freigegeben.

Du machst eine Ausnahme.

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?
    }
}

Beachten Sie, dass es sich in der Tat um eine schreckliche Idee handelt. Sie sollten das Design besser überdenken. Ich bin mir sicher, dass es einen besseren geben muss.

EDIT

Ich habe einen Fehler gemacht, das Auslösen von Ausnahme stoppt nicht die Zerstörung seiner Basen und Mitglieder, sondern macht es nur möglich, das Prozessergebnis von Class2s Dtor zu erhalten. Und was man damit machen könnte, ist noch nicht klar.

45
songyuanyao
~Foo(){
   return;
}

bedeutet genau dasselbe wie:

~Foo() {}

Sie ähnelt einer void-Funktion. Das Erreichen des Endes ohne eine return;-Anweisung ist dasselbe, als hätte man return; am Ende.

Der Destruktor enthält Aktionen, die ausgeführt werden, wenn das Zerstören einer Foo bereits begonnen hat. Es ist nicht möglich, einen Zerstörungsvorgang abzubrechen, ohne das gesamte Programm abzubrechen.

24
M.M

[D] Oes, die explizit vom Destruktor zurückkehren, bedeutet, dass wir sie niemals zerstören wollen?

Nein. Eine vorzeitige Rückgabe (über return; oder throw ...) bedeutet nur, dass der Rest des Destruktors nicht ausgeführt wird. Die Basis und die Mitglieder werden immer noch zerstört und ihre Destruktoren laufen weiter, siehe [except.ctor]/3 .

Für ein Objekt des Klassentyps mit beliebiger Speicherdauer, dessen Initialisierung oder Zerstörung durch eine Ausnahme beendet wird, wird der Destruktor für jedes der vollständig erstellten Unterobjekte des Objekts aufgerufen.

Nachstehend finden Sie Codebeispiele für dieses Verhalten.


Ich möchte es so machen, dass ein bestimmtes Objekt nur durch einen Zerstörer anderer Objekte zerstört wird, d. H. Nur wenn das andere Objekt bereit ist, zerstört zu werden.

Es hört sich an, als ob die Frage in der Frage des Eigentums begründet ist. Löschen des "Besessenen" -Objekts nur dann, wenn das übergeordnete Objekt in einer sehr gebräuchlichen Sprache zerstört und mit einem der folgenden Objekte erreicht wird (jedoch nicht beschränkt auf);

  • Zusammensetzung, es ist eine automatische Membervariable (d. H. "Stapelbasiert")
  • Ein std::unique_ptr<>, um das ausschließliche Eigentum des dynamischen Objekts auszudrücken
  • Ein std::shared_ptr<>, um den gemeinsamen Besitz eines dynamischen Objekts auszudrücken

In Anbetracht des Codebeispiels im OP kann der std::unique_ptr<> eine geeignete Alternative sein.

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.
}

Ich vermerke die if-Zustandsüberprüfung im Beispielcode. Es deutet an, dass der Staat an den Besitz und die Lebensdauer gebunden ist. Sie sind nicht alle dasselbe; Sicher, Sie können das Objekt, das einen bestimmten Status erreicht, an die "logische" Lebensdauer binden (d. h. einen Bereinigungscode ausführen), aber ich würde die direkte Verbindung zum Besitz des Objekts vermeiden. Es ist möglicherweise eine bessere Idee, einige der hier verwendeten Semantiken zu überdenken oder die "natürliche" Konstruktion und Zerstörung zuzulassen, um den Anfangs- und Endzustand des Objekts zu bestimmen.

Randnotiz; Wenn Sie nach einem Status im Destruktor suchen müssen (oder eine Endbedingung geltend machen), besteht eine Alternative zu throw darin, std::terminate (mit etwas Protokollierung) aufzurufen, wenn diese Bedingung nicht erfüllt ist. Dieser Ansatz ähnelt dem Standardverhalten und -ergebnis, wenn beim Abwickeln des Stapels infolge einer bereits ausgelösten Ausnahme eine Ausnahme ausgelöst wird. Dies ist auch das Standardverhalten, wenn ein std::thread mit einer nicht behandelten Ausnahme beendet wird.


[D] Oes, die explizit vom Destruktor zurückkehren, bedeutet, dass wir sie niemals zerstören wollen?

Nein (siehe oben). Der folgende Code veranschaulicht dieses Verhalten. hier verlinkt und eine dynamische Version . Die noexcept(false) wird benötigt, um zu vermeiden, dass std::terminate() aufgerufen wird.

#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;
    }
}

Hat die folgende Ausgabe;

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

Gemäß dem C++ - Standard (12.4 Destruktoren)

8 Nachdem der Körper des Destruktors ausgeführt und ein beliebiger .__ zerstört wurde. automatische Objekte, die innerhalb des Körpers zugewiesen werden, ein Destruktor für Klasse X ruft die Destruktoren für die direkten nichtvarianten nicht statischen Daten von X auf Mitglieder, die Destruktoren für die direkten Basisklassen von X und, falls X die .__ ist. Typ der am meisten abgeleiteten Klasse (12.6.2), ruft ihr Destruktor die .__ auf. Destruktoren für die virtuellen Basisklassen von X. Alle Destruktoren heißen als würden sie mit einem qualifizierten Namen referenziert, das heißt, ignoriert mögliche virtuelle überschreibende Destruktoren in abgeleiteten Klassen . Stützpunkte und Mitglieder werden in umgekehrter Reihenfolge des Abschlusses zerstört ihres Konstruktors (siehe 12.6.2). Eine return-Anweisung (6.6.3) in einer Destruktor kehrt möglicherweise nicht direkt zum Anrufer zurück. Vor Übertragen der Kontrolle an den Aufrufer, die Destruktoren für die Mitglieder und Basen werden aufgerufen. Destruktoren für Elemente eines Arrays heißen in umgekehrter Reihenfolge ihres Aufbaus (siehe 12.6).

Eine zurückkehrende Anweisung verhindert also nicht, dass das Objekt, für das der Destruktor aufgerufen wird, zerstört wird.

8

bedeutet das explizite Zurückkehren vom Zerstörer bedeutet, dass wir ihn niemals zerstören wollen.

Nein. 

Der Destruktor ist eine Funktion, mit der Sie das Schlüsselwort return verwenden können, das die Zerstörung des Objekts jedoch nicht verhindern kann. Sobald Sie sich im Destruktor befinden, zerstören Sie bereits Ihr Objekt. Jede Logik, die dies verhindern möchte, muss dies vor auftreten.

Aus irgendeinem Grund denke ich intuitiv, dass Ihr Design-Problem mit einem shared_ptr und möglicherweise einem benutzerdefinierten Deleter gelöst werden kann, aber dies würde mehr Informationen über das besagte Problem erfordern.

7
Drax

Wie alle anderen darauf hingewiesen haben, ist return keine Lösung. 

Als Erstes möchte ich hinzufügen, dass Sie sich normalerweise keine Sorgen machen sollten. Es sei denn, Ihr Professor hat ausdrücklich gefragt. Es wäre sehr merkwürdig, wenn Sie der externen Klasse nicht vertrauen könnten, Ihre Klasse nur zum richtigen Zeitpunkt zu löschen, und ich denke, niemand sieht sie. Wenn der Zeiger herumgereicht wird, würde der Zeiger sehr wahrscheinlich shared_ptr/weak_ptr sein und die Klasse im richtigen Moment zerstören lassen. 

Aber hey, es ist gut zu fragen, wie wir ein seltsames Problem lösen würden, wenn es jemals auftauchen würde, wenn wir etwas lernen würden (und keine Zeit damit verschwenden, eine Frist einzuhalten!).

Was also für eine Lösung? Wenn Sie dem Destruktor von Class1 zumindest nicht vertrauen können, um Ihr Objekt nicht zu früh zu zerstören, können Sie einfach den Destruktor von Class2 als privat deklarieren und dann den Destruktor von Class1 als Freund von Class2 wie folgt deklarieren:

class Class2;

class Class1 {

    Class2* myClass2;

public:
    ~Class1();
};

class Class2 {
private:
        ~Class2();

friend Class1::~Class1();
};

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

Class2::~Class2() {
}

Als Bonus benötigen Sie nicht das Status-Flag. Was ist gut - wenn jemand wollte, dass sich jemand so schlecht mit Ihnen schmeißt, warum sollte er das Statusflag nicht auf FINISHED setzen, und dann delete aufrufen? 

Auf diese Weise haben Sie tatsächlich die Garantie, dass das Objekt nirgendwo anders als im Destruktor von Class1 zerstört werden kann. 

Natürlich hat der Destruktor von Class1 Zugriff auf alle privaten Mitglieder von Class2. Möglicherweise spielt es keine Rolle - schließlich wird Class2 sowieso zerstört! Wenn dies aber der Fall ist, können wir noch kompliziertere Wege beschwören, um es zu umgehen. warum nicht. Zum Beispiel:

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() {
}

Auf diese Weise sind die öffentlichen Mitglieder weiterhin in der abgeleiteten Klasse verfügbar, die privaten Mitglieder sind jedoch tatsächlich privat. ~ Class1 erhält nur Zugriff auf die privaten und geschützten Mitglieder von Class2 und die geschützten Mitglieder von Class2Hidden. was in diesem Fall nur die Zerstörer ist. Wenn Sie ein geschütztes Mitglied von Class2 vor dem Destruktor von Class1 schützen müssen ... gibt es verschiedene Möglichkeiten, aber es hängt wirklich davon ab, was Sie tun.

Viel Glück!

1
Francesco Dondi

Natürlich nicht. Der explizite Aufruf von 'return' entspricht 100% der impliziten Rückkehr nach der Ausführung des Destruktors.

1
Trantor

Sie können eine neue Methode erstellen, um das Objekt zum "Selbstmord begehen" zu machen und den Destruktor leer zu lassen, sodass etwas wie die gewünschte Aufgabe ausgeführt wird:

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

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

 Class2::~Class2()
 {
 }
1
Hasson

Alle stapelbasierten Objekte werden zerstört, unabhängig davon, wie schnell Sie vom Destruktor return abgehen. Wenn Sie delete dynamisch zugewiesene Objekte vermissen, kommt es zu absichtlichem Speicherverlust. 

Das ist die ganze Idee, wie Move-Konstruktoren funktionieren würden. Die Bewegung CTOR würde einfach den Speicher des ursprünglichen Objekts übernehmen. Der Destruktor des ursprünglichen Objekts wird einfach delete aufrufen.

1
Ajay

Nein. return bedeutet nur das Verlassen der Methode, die Zerstörung des Objekts wird jedoch nicht angehalten. 

Warum sollten Sie auch wollen? Wenn das Objekt auf dem Stack zugewiesen wurde und Sie die Zerstörung irgendwie beenden konnten, lebte das Objekt in einem wiederhergestellten Teil des Stacks, der wahrscheinlich vom nächsten Funktionsaufruf überschrieben wird, der den gesamten Objektspeicher überschreiben und undefiniertes Verhalten verursachen kann .

Wenn das Objekt auf dem Heapspeicher zugeordnet ist und Sie die Zerstörung verhindern konnten, hätten Sie einen Speicherverlust, da der Code, der delete aufruft, davon ausgehen würde, dass er sich nicht an einem Zeiger auf das Objekt festhalten muss, solange es noch vorhanden ist und Gedächtnis aufnehmen, auf das sich niemand bezieht.

1
Sean

In diesem Fall könnten Sie eine klassenspezifische Überladung des Löschoperators verwenden. Also für dich Class2 könntest du sowas

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;
}

Wenn Sie dann den Wert "finished" vor dem Löschen auf "true" setzen, wird der eigentliche Löschvorgang aufgerufen. Beachten Sie, dass ich es nicht getestet habe, ich habe nur den Code geändert, den ich hier gefunden habe http://en.cppreference.com/w/cpp/memory/new/operator_delete

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