Zugriffsverweigerung bei Destruktor



  • Nein, den hab ich nicht (hab neben diesem Enemy-Konstruktor keinen andern für die Klasse mehr). Bis jetzt bin ich immer schön ohne Kopierkonstruktor ausgekommen.

    Wäre das einfach so (mit entsprechender Deklaration):

    Enemy::Enemy(const Enemy &NewEnemy)
    {
       Barrel = new sf::Sprite(Image);
    }
    

    Aber sobald ich den Kopierkonstruktor verwende, muss ich doch alle Variabeln der Klasse manuell kopieren, oder gibt es eine Möglichkeit, nur einzelne manuell handzuhaben? Ich hätte sonst eine riesige Zuweisungsliste...

    Ausserdem, einige Zuweisungen funktionieren gar nicht (wegen der Referenz glaube ich). Wie müsste ich es innerhalb des Kopierkonstruktors schreiben, um z.B. Folgendes zu tun:

    Type = NewEnemy.GetType();
    

    wobei GetType eine short-Variable zurückgibt:

    Enemy::GetType()
    {
       return Type; // Type ist vom Typ short int
    }
    

    Ich erhalte immer eine Fehlermeldung: error C2662: 'Enemy::GetType': this-Zeiger kann nicht von 'const Enemy' in 'Enemy &' konvertiert werden

    Und da muss ich doch noch einen separaten, zweiten Destruktor machen, oder? Was würde dann in dem stehen?



  • Ja, dein Copy-Ctor würde in etwa so aussehen. Du musst dich einfach fragen was du alles machen musst, um von dem übergebenen Parameter eine Kopie zu erzeugen. Bei Pointern musst du also Speicher anfordern und initialisieren und alles andere einfach mittels des Parameters kopieren. Im Grunde also immer ein this->foo = parameter->foo;

    Die Fehlermeldung bekommst du, weil deine Referenz konstant ist, GetType jedoch nicht. Mach entweder GetType const oder greif direkt auf Type zu (also NewEnemy.Type)



  • Nexus schrieb:

    Nein, den hab ich nicht (hab neben diesem Enemy-Konstruktor keinen andern für die Klasse mehr). Bis jetzt bin ich immer schön ohne Kopierkonstruktor ausgekommen.

    Wäre das einfach so (mit entsprechender Deklaration):

    Enemy::Enemy(const Enemy &NewEnemy)
    {
       Barrel = new sf::Sprite(Image);
    }
    

    statt einer zuweisung solltest du in konstruktoren immer die elementinitialisierungsliste verwenden:

    Enemy::Enemy(const Enemy& NewEnemy)
    : Barrel(new sf::Sprite(Image))
    {
    }
    

    wenn du mehr als einen member dynamisch per new initialisierst, ist die "länge" des copy-ctors außerdem noch dein geringstes problem:

    class Foo
    {
       int *a, *b;
    public:
       Foo (Foo const& other)
       : a(new int(other.a)), b(new int(other.b))
       { 
       }
    };
    

    falls new int(other.b) fehlschlägt, wer löscht dann new int(other.a) ?
    eventuell könntest du an dieser stelle auch überlegen, smart-pointer zu verwenden. allerdings möchte ich dir nicht in deinem design rumpfuschen, dafür weiß ich zuwenig über deine anforderungen.

    Ausserdem, einige Zuweisungen funktionieren gar nicht (wegen der Referenz glaube ich). Wie müsste ich es innerhalb des Kopierkonstruktors schreiben, um z.B. Folgendes zu tun:

    Type = NewEnemy.GetType();
    

    wobei GetType eine short-Variable zurückgibt:

    Enemy::GetType()
    {
       return Type; // Type ist vom Typ short int
    }
    

    Ich erhalte immer eine Fehlermeldung: error C2662: 'Enemy::GetType': this-Zeiger kann nicht von 'const Enemy' in 'Enemy &' konvertiert werden

    ganz einfach, die methode Enemy::GetType konstant machen, wie this->that schon vorgeschlagen hat.
    das geht so:

    class Enemy
    {
    //...
       short GetType () const
       { 
          return Type;
       }
    };
    

    Und da muss ich doch noch einen separaten, zweiten Destruktor machen, oder? Was würde dann in dem stehen?

    eine klasse kann nur einen destruktor haben. was meinst du also?



  • Okay, ich dachte, es läge an der Referenz, zumal in der Fehlermeldung auch Enemy mit Enemy& verglichen wird. Merkwürdig, dass const short GetType() nicht geht, short GetType() const aber schon... Ich dachte, das spiele keine Rolle.

    Aber wieso muss GetType() konstant sein? Beim direkten Zugriff wäre ja Type auch nicht konstant, oder? Wenn wir gerade dabei sind, was ist genau der Unterschied zwischen short GetType() const und short GetType()? Die nicht-konstante Version kann ja auch nicht verändert werden (kein L-Value).

    queer_boy schrieb:

    wenn du mehr als einen member dynamisch per new initialisierst, ist die "länge" des copy-ctors außerdem noch dein geringstes problem:

    class Foo
    {
       int *a, *b;
    public:
       Foo (Foo const& other)
       : a(new int(other.a)), b(new int(other.b))
       { 
       }
    };
    

    falls new int(other.b) fehlschlägt, wer löscht dann new int(other.a) ?
    eventuell könntest du an dieser stelle auch überlegen, smart-pointer zu verwenden. allerdings möchte ich dir nicht in deinem design rumpfuschen, dafür weiß ich zuwenig über deine anforderungen.

    Okay, das wäre dann wohl schon zu kompliziert (noch nie von Smart-Pointern gehört), da würde es sich fast eher lohnen, Barrel nicht als Zeiger zu verwenden, sondern als normale Variable. Trotzdem danke, vielleicht werde ich das später mal berücksichtigen... Aber wieso sollte das new überhaupt fehlschlagen?

    queer_boy schrieb:

    Und da muss ich doch noch einen separaten, zweiten Destruktor machen, oder? Was würde dann in dem stehen?

    eine klasse kann nur einen destruktor haben. was meinst du also?

    Sorry, da hab ich was falsch verstanden, ich dachte ich müsste den im Kopierkonstruktor per new sf::Sprite definierten Zeiger auch noch deleten, aber das wird wohl schon im (einzigen) Destruktor getan...



  • bei einer membermethode wird auch (implizit) der this-zeiger übergeben. tatsächlich sieht der aufruf einer member-funktion dann so aus (in C):

    struct Enemy
    {
       short Type;
    };
    
    short GetType (Enemy* this)
    {
      return this->Type;
    }
    
    Enemy x;
    GetType (&x);
    //und das ist äquivalent zu (C++) x.GetType();
    

    das problem ist nun, dass es sehr wohl auch konstante Enemy-objekte geben kann (oder zeiger darauf). das wäre kein problem, wenn man GetType einfach für konstante Enemy-objekte überladen könnte:

    short GetType (const Enemy* this)
    {
       return this->Type;
    }
    

    genau das erlaubt die C++-syntax auf diese art und weise:

    class Enemy
    {
    public:
       short GetType () const; //this zeigt auf const Enemy
       short GetType (); //this zeigt auf Enemy (non-const)
    };
    

    tatsächlich lassen sich von einer (member)funktion sowohl konstante als auch nicht-konstante versionen definieren (also überladen).

    da du in deinem copy-ctor eine referenz auf ein konstantes Enemy hast, kannst du für dieses Enemy auch nur konstante memberfunktionen aufrufen.

    Aber wieso sollte das new überhaupt fehlschlagen?

    in diesem fall würde new nur fehlschlagen können, wenn es keinen speicher mehr gibt. selten, aber doch möglich.
    anders sieht es schon aus, wenn du benutzerdefinierte klassen verwendest (und nicht wie in meinem beispiel ints), deren konstruktor eine ausnahme wirft:

    class Naughty
    {
    public:
       Naughty (int x)
       {
         if (x != 42) throw "this is not the answer!";
       }
    };
    
    class Enemy
    {
       Naughty *n, *m;
    public:
       Enemy () : n(new Naughty(42)), m(new Naughty(23)) {}
    };
    
    try
    {
     Enemy e;
    } catch (...) 
    {
    }
    

    schwupps, schon hast du ein memory-leak, und das ist oft nicht so einfach zurückzuverfolgen.

    lies dir dazu einfach mal was über smart-pointer und exception safety durch 🙂



  • queer_boy schrieb:

    statt einer zuweisung solltest du in konstruktoren immer die elementinitialisierungsliste verwenden:

    Enemy::Enemy(const Enemy& NewEnemy)
    : Barrel(new sf::Sprite(Image))
    {
    }
    

    wenn du mehr als einen member dynamisch per new initialisierst, ist die "länge" des copy-ctors außerdem noch dein geringstes problem:

    class Foo
    {
       int *a, *b;
    public:
       Foo (Foo const& other)
       : a(new int(other.a)), b(new int(other.b))
       { 
       }
    };
    

    falls new int(other.b) fehlschlägt, wer löscht dann new int(other.a) ?

    genau wegen solcher Dinge gehören news für normale pointer nicht in die Initialisierungsliste. Wenn innerhalb der Initialisierungsliste eine Exception ausgelöst wird, hat man sich sofort ein Speicherleck eingehandelt. Selbst im Ctor Body muss man dann noch mit einem lokalen try/catch Block fehlgeschlagene news abfangen. Deshalb bei lebenswichtigen Pointermembern lieber gleich smartpointer verwenden.
    Mehr zum Thema: http://www.gotw.ca/gotw/066.htm



  • pumuckl schrieb:

    ...
    Mehr zum Thema: http://www.gotw.ca/gotw/066.htm

    ebenda schrieb:

    ...
    The short answer is: By using a function-try-block.

    For example, here's how we can write a function-try-block for C's constructor:

    // Example 1(a): Constructor function-try-block
        //
        C::C()
        try
          : A ( /*...*/ ) // optional initialization list
          , b_( /*...*/ )
        {
        }
        catch( ... )
        {
          // We get here if either A::A() or B::B() throws.
    
          // If A::A() succeeds and then B::B() throws, the
          // language guarantees that A::~A() will be called
          // to destroy the already-created A base subobject
          // before control reaches this catch block.
        }
    

    😮 😮 😮 😮 😮
    Wieder mal bestätigt sich "Man lernt nie aus" !!!
    Von der Konstruktion hatte ich noch nie gehört.

    Gleich ausprobiert (g++ 3.3.3) und (für mich) Erstaunliches festgestellt: Dieser catch-Block "fängt" zwar die Exception, sie fliegt danach aber trotzdem weiter.

    Wie auc hier steht:

    ... What's less obvious, but clearly stated in the standard, is that if the catch block does not throw ... and control reaches the end of the catch block of a constructor or destructor, then the original exception is automatically rethrown.

    da muss ich nochmal ein wenig mit rumspielen und -lesen.
    👍 😋

    Danke,

    Simon2.



  • Vielen Dank für die zahlreichen Antworten! Ich hätte nicht gedacht, dass wegen eines einzigen Zeigers ein solcher Mehraufwand erforderlich ist 😉
    Aber das ist sowieso gerade eine gute Übung für mich, zumal ich bis jetzt noch nicht viel von Kopierkonstruktor, konstanten Funktionen etc. wusste.

    Doch noch was zu den const-Funktionen: Ich verstehe schon, dass in diesem Zusammenhang (wenn Enemy konstant ist) auch die Memberfunktionen einen konstanten Rückgabewert haben müssen. Jedoch gebe ich bei meinen Funktionen mit Rückgabetyp sowieso immer einen Wert zurück, spricht da etwas dagegen, alle Funktionen à la GetAnything() konstant zu deklarieren? Wäre das nur bei der Rückgabe von Zeigern oder Referenzen von Bedeutung? Ich verstehe nicht ganz, welchen tatsächlichen Unterschied es macht ob ich jetzt short GetType() oder short GetType() const schreibe (ich meine allgemein, nicht nur wenn die Klasse selber konstant übergeben wird)... solange ich in der Funktion nur return Type; aufgeschrieben habe, kann ja eh nichts verändert werden, oder?

    Noch was: ist const short GetType() const das gleiche wie short GetType() const?



  • Wenn eine Memberfunktion const deklariert ist bedeutet das nicht, dass der Rückgabewert const ist, sondern, das die Funktion aufgerufen wird, wenn die dahinterliegende Instanz const ist. Es darf also in einer solchen Funktion kein Member der Klasse verändert werden.



  • Nexus schrieb:

    Vielen Dank für die zahlreichen Antworten! Ich hätte nicht gedacht, dass wegen eines einzigen Zeigers ein solcher Mehraufwand erforderlich ist ;)...

    Da merkst Du mal, welch Vorteil es ist, "Objekte"/Stackvariablen zu haben. 😃
    Das Problem macht nämlich nicht der Zeiger, sondern die (Möglichkeit der) dynamische(n) Verwaltung.
    Sie hat auch Vorteile, aber die kosten eben auch etwas (wie z.B. hier).

    Gruß,

    Simon2.



  • @ Simon2
    Mit der Zeit werde ich kaum darum herum kommen, deshalb ist es nicht mal so schlecht... Ich frag mich nur ein bisschen, da ich bis jetzt schon ganze 2D-Spiele geschrieben habe, ohne jemals etwas von Kopierkonstruktor etc. gehört zu haben 😃

    @ Braunstein
    Danke für die Erläuterung, das hab ich nicht ganz richtig begriffen. Ich versteh folgenden Unterschied aber immer noch nicht, hat es hier etwas mit dem Rückgabetyp zu tun (da ja das Schlüsselwort const vorne steht):

    const short GetType() const;
    short GetType() const;
    

    Zudem ist mir immer noch nicht klar, was dafür spricht, Memberfunktionen wie GetType(), die nur einen Wert zurückgeben, als nicht-konstant zu deklarieren.



  • Nexus schrieb:

    Zudem ist mir immer noch nicht klar, was dafür spricht, Memberfunktionen wie GetType(), die nur einen Wert zurückgeben, als nicht-konstant zu deklarieren.

    Gar nichts. Diese Funktionen sollten auch immer const sein. Es gibt auch da sicher Ausnahmefälle (lazy evaluation?).

    const short GetType() const;
    

    Konstante Memberfunktion die ein const short zurückgibt. Das wäre hier recht unsinnig, da der Rückgabewert eh kopiert wird.

    short GetType() const;
    

    Konstante Memberfunktion die ein short (nicht konstant) zurückgibt.
    In diesem Fall sind aber beide Funktionen identisch. Das wären ja auch keine möglichen Überladungen, da der Rückgabetyp ja nicht zur Funktionssignatur gehört.
    Sinn machen konstante Rückgabewerte ja eher bei Referenzen oder Zeigern.



  • Okay, vielen Dank für die schnelle Antwort!
    Dann mache ich eben alle GetBlabla-Funktionen konstant 😃


  • Mod

    Braunstein schrieb:

    const short GetType() const;
    

    Konstante Memberfunktion die ein const short zurückgibt. Das wäre hier recht unsinnig, da der Rückgabewert eh kopiert wird.

    short GetType() const;
    

    Konstante Memberfunktion die ein short (nicht konstant) zurückgibt.
    In diesem Fall sind aber beide Funktionen identisch.

    Treffsicher daneben.
    Beide Deklarationen deklarieren ein und dieselbe Funktion. (Top-level) cv-Qualifizierer des Rückgabewertes sind nicht Teil des Funktionsstyps, wenn der Rückgabewert skalar ist. Das wird klar, wenn man sich überlegt, dass das Ergebnis eines entsprechenden Funktionsaufrufes ein rvalue ist, und skalare rvalues sind niemals cv-qualifiziert (weil es keine Objekte sondern nur der Wert an sich - das Bitmuster, wenn man so will - sind).



  • Okay, das verwirrt mich jetzt ein bisschen...
    Versteh ich dich richtig, dass nur ein Wert (R-Value) zurückgegeben wird, und es keine Rolle spielt, ob der konstant ist oder nicht, weil er sowieso nicht verändert wird? (Sorry kenn mich eben nicht so aus mit deinen Fachausdrücken :D).

    Heisst das in diesem Fall, man kann das erste const von const short GetType() const immer weglassen? Wieso ermöglicht es dann die C++-Syntax, es vorher zu schreiben, wenn es so oder so überflüssig ist?


  • Mod

    Heisst das in diesem Fall, man kann das erste const von const short GetType() const immer weglassen?

    ja, weil short ein skalarer Typ ist. Das besondere an skalaren Typen ist, dass sie eine einzelne unteilbare Einheit bilden: man kann hier also sehr schön trennen zwischen dem Speicher (=Objekt) eines solchen Typs und dem Zustand dieses Speichers. Ein Ausdruck skalaren Typs (bei Klassen/Arrays/Funktionen/Referenzen wird es komplizierter) kann nun (ganz allgemein) entweder auf ein Objekt (=Speicherstelle) verweisen oder nur den Wert (=Zustand) an sich bezeichnen. Ersteres stellt einen lvalue-Ausdruck dar, letzteres ein rvalue. Nun ist aber klar, dass die Eigenschaft, konstant oder volatile zu sein, nur bestimmtem Speicher zukommen kann. Bezogen auf einen bestimmten Zustand (man denke zum Beispiel auch ein ein einfaches Literal wie 1 - dem entspricht ein bestimmtes Bitmuster vom Typ int) sind diese Begriffe sinnlos. Die Konsequenz ist, dass solche (skalaren) Rvalues niemals cv-qualifiziert sind. Immer dann, wenn einer Konvertierung eines Lvalues in ein Rvalue erfolgt (das entspricht im Allgemeinen dem Lesen einer Variablen, also der Bestimmung des Zustands einer bestimmten Speicherstelle), gehen evtl. vorhandene Qualifikationen des Objektes verloren, nach dem Lesevorgang haben diese keine Bedeutung mehr.
    Der Rest ergibt sich damit im Grunde von selbst. Der Compiler unterscheidet nicht zwischen beiden Deklarationen, weil er es gar nicht kann.

    Nexus schrieb:

    Wieso ermöglicht es dann die C++-Syntax, es vorher zu schreiben, wenn es so oder so überflüssig ist?

    Das ist keine syntaktische Frage. Der Rückgabetyp könnte ja auch durch ein typedef konstant erscheinen (der tatsächlich deklarierte Rückgabetyp ist auch dann wiederum unqualifiziert), dann sieht man das der Deklaration gar nicht unmittelbar an. Zudem, was wichtiger ist, ist nicht jeder Ausdruck skalar, und für bestimmte Typen (primär Klassen) existieren cv-qualifizierte Rvalues.

    Bleibt noch anzumerken, dass es - meiner Meinung nach - fast nie sinnvoll ist, den Rückgabetyp konstant zu machen (wie oben geklärt, spielt es sowieso nur bei Klassen eine Rolle). Die Gründe dafür sind nicht sonderlich kompliziert. Hauptsächlich geht es um Kapselung - eine Funktion sollte so wenig wie möglich vom Aufrufer verlangen (=möglichst allgemeiner Parameter) und möglichst viel versprechen (=Garantien über die Rückgabe liefern, mit einem veränderbaren Objekt kann man mehr machen, als mit einem konstanten), aber das sprengt ein bisschen den Rahmen dieser Diskussion.



  • Okay, vielen Dank für diese umfassende Antwort!



  • camper schrieb:

    Bleibt noch anzumerken, dass es - meiner Meinung nach - fast nie sinnvoll ist, den Rückgabetyp konstant zu machen (wie oben geklärt, spielt es sowieso nur bei Klassen eine Rolle). Die Gründe dafür sind nicht sonderlich kompliziert. Hauptsächlich geht es um Kapselung - eine Funktion sollte so wenig wie möglich vom Aufrufer verlangen (=möglichst allgemeiner Parameter) und möglichst viel versprechen (=Garantien über die Rückgabe liefern, mit einem veränderbaren Objekt kann man mehr machen, als mit einem konstanten), aber das sprengt ein bisschen den Rahmen dieser Diskussion.

    Und deshalb ist es gut
    (a+b)=c;
    zu erlauben?


  • Mod

    Shade Of Mine schrieb:

    camper schrieb:

    Bleibt noch anzumerken, dass es - meiner Meinung nach - fast nie sinnvoll ist, den Rückgabetyp konstant zu machen (wie oben geklärt, spielt es sowieso nur bei Klassen eine Rolle). Die Gründe dafür sind nicht sonderlich kompliziert. Hauptsächlich geht es um Kapselung - eine Funktion sollte so wenig wie möglich vom Aufrufer verlangen (=möglichst allgemeiner Parameter) und möglichst viel versprechen (=Garantien über die Rückgabe liefern, mit einem veränderbaren Objekt kann man mehr machen, als mit einem konstanten), aber das sprengt ein bisschen den Rahmen dieser Diskussion.

    Und deshalb ist es gut
    (a+b)=c;
    zu erlauben?

    Ich sehe daran nicht so viel Schlimmes. Die geerbte C-Syntax erlaubt ohnehin eine Menge Mist. Schreibt man das mit Klammern, ist sowieso Machiavelli am Werk - oder jemand mit einem Klammerfetisch (etwas, das ich nicht besonders mag, es kann helfen Präzedenz zu klären, aber häufig wird ein Ausdruck auch unverständlicher, wenn man nur nach am Klammerabzählen ist). Gefährlich könnte es werden, wenn wir eigentlich einen Vergleich schreiben wollten. Das passiert für gewöhnlich in if,while-statements; und hier sind moderne Compiler in der Regel sehr mitteilungsfreudig - es ist also imo kein praktisches Problem, wenn man die Warnungen des Compilers gelegentlich liest. Es bliebe der Fall, wenn der eigentlich gewollte Vergleich nur ein Subausdruck ist. Solche Fälle sind eher selten und per se schwerer zu verstehen, hier ist in jedem Falle erhöhte Aufmerksamkeit gefordert, oder man verzichtet darauf - auch ein C-Erbe i.w.S.

    Wäre es gut, es nicht zu erlauben? Sicherlich, aber das Mittel ist ungeeignet. Der Defekt, wenn man das so bezeichnen will, liegt ja beim Zuweisungsoperator, nicht bei der Addition.
    Und dann wäre da noch
    c=a+b
    Ist obiges Problem wirklich so gravierend, dass wir mit Freude auf Move-Kopiererei verzichten würden? Da habe ich große Zweifel.
    Persönlich finde ich, dass das a+b=c Problem unnötig aufgebauscht wurde. Hier wird eine relativ isolierte Problematik zum Anlass genommen, potentiell das Design nahezu jeder Funktion zu beeinflussen.



  • camper schrieb:

    Ist obiges Problem wirklich so gravierend, dass wir mit Freude auf Move-Kopiererei verzichten würden?

    Warum sollte ein move nicht funktionieren? Wenn ich mir zB Mojo ansehe - dann wuerde (a+b)=c wohl sowieso nicht klappen.

    oder uebersehe ich etwas? welchen nachteil hat es, ein (a+b)=c zu verbieten und sich so an die builtin semantic zu halten. denn zuweisung an temopraere objekte ist immer boese. es sei denn jemand hat eine situation wo das sinn machen kann...


Anmelden zum Antworten