Polymorphismus bei Arrays



  • Hallo,

    ich lese zur Zeit "More Effective C++" von Meyers und bin am dritten Kapitel hängen geblieben.
    Meyers schreibt (S. 18 unten), dass das Verhalten beim Löschen von Arrays über einen Basisklassenzeiger undefiniert ist. Polymorphie funktioniert hier also nicht.
    Vorher erklärt er, dass bei über einen Basisklassenzeiger angesprochene Arrays die Zeigerarithmetik nicht funktioniert, weil nicht bekannt ist, wie groß ein abgeleites Objekt ist.

    Warum hier die Polymorphie aussetzt, hab ich leider nicht ganz kapiert. Kann mir das vielleicht einer erklären?

    Danke 🙂



  • class A
    {
    public:
      int a;
      virtual ~A() {}
    };
    
    class B : public A
    {
      int b;
    };
    
    void g() {
      B arr[3];
      A* pa1 = arr;
      A* pa2 = pa1 + 1; // Ptr-Arithmetik auf A*
      A* pa3 = arr + 1; // Ptr-Arithmetik auf B*
      pa1->a = 23; // OK
      pa3->a = 99; // OK
      pa2->a = 42; // Ka-Boom!
    }
    
    pa1     pa2        pa3
      |       |          |
      |       |          |
      V       V          V
    +-[B]--------------+-[B]--------------+-[B]--------------+
    | +-[A]---+        | +-[A]---+        | +-[A]---+        |
    | | int a |  int b | | int a |  int b | | int a |  int b |
    | +-------+        | +-------+        | +-------+        |
    +------------------+------------------+------------------+
    


  • Danke für die Mühe.

    Wenn ich das richtig sehe, liegt der Hund da begraben, dass die Zeigerarithmetik sich nur um den statischen Typen des Zeigers kümmert. (Kann man das so sagen?) Deswegen wird der Zeiger hier unterschiedlich verrückt

    A* pa2 = pa1 + 1; // Ptr-Arithmetik auf A*
      A* pa3 = arr + 1; // Ptr-Arithmetik auf B*
    

    , auch wenn

    A* pa1 = arr;
    

    gilt.

    Danke



  • Ja.
    Woher soll das Array denn wissen wie groß ein Objekt ist?

    teste mal ein bisschen mit sizeof() herum, dann wirst du es sehen.



  • Hi,

    eine kleine Mißverständnisfalle mal angegangen:

    Shade Of Mine schrieb:

    ...Woher soll das Array denn wissen wie groß ein Objekt ist?...

    Das klingt ein wenig mißverständlich, weil Sebastians Beispiel etwas einfacher ist als das Original, indem es nur mit einem Zeiger (pa1 bzw. pa2) arbeitet statt mit einem "Zeigerarray".

    Deine Aussage auf Sebastians Beispiel gedeutet: "Woher soll pa2 wissen, wie groß ein Objekt ist?"
    (arr "weiß" naturlich, wie groß seine Objekte sind).

    Stimmt doch, oder?

    Gruß,

    Simon2.



  • Woher soll das Array denn wissen wie groß ein Objekt ist?

    Dem Einwand von Simon2 stimme ich zu. Ich dachte nur, dass bei

    Base *bptr = new Derived;
    

    der Zeiger ja auch "weiß", wie groß der Speicherbereich ist, auf den er verweist.

    Aber ich denke, bei

    B arr[3];
    A* pa1 = arr;
    A* pa2 = pa1 + 1; // Ptr-Arithmetik auf A*
    

    ist C++ in einem Dilemma, weil es sich entscheiden muss, ob anhand des dynamischen oder des statischen Typs von pa1 weitergezählt wird. Und da entscheidet es sich für den (offensichtlicheren) statischen Weg.

    Stimmt das so oder ist das ein grobes Missverständnis der Mächtigkeit von Polymorphie?



  • moagnus schrieb:

    [...] ist C++ in einem Dilemma, weil es sich entscheiden muss, ob anhand des dynamischen oder des statischen Typs von pa1 weitergezählt wird. Und da entscheidet es sich für den (offensichtlicheren) statischen Weg.

    Stimmt das so oder ist das ein grobes Missverständnis der Mächtigkeit von Polymorphie?

    Es klingt so, als denkst Du, Zeigerarithmetik gehöre bei Polymorphie irgendwie dazu. Nenne mir eine Programmiersprache, die Zeigerarithmetik bietet und Zeiger auf polymorphe Objekte gesondert behandelt. 🙂

    Gruß,
    SP



  • moagnus schrieb:

    Dem Einwand von Simon2 stimme ich zu. Ich dachte nur, dass bei

    Base *bptr = new Derived;
    

    der Zeiger ja auch "weiß", wie groß der Speicherbereich ist, auf den er verweist.

    Dann frag ihn mal:
    sizeof(*bptr) wird dir sizeof(Base) und nicht sizeof(Derived) liefern.

    Das ist normalerweise aber kein Problem. Ein Problem wird es nur wenn du sagst: gehe um 5*sizeof(*bptr) nach vorne, also ein
    bptr+=5;
    machst.

    PS:
    du redest von statischen und dynamischen Typen. Ein Zeiger aber ist dumm. Der weiss garnichts. Er weiss nur wie groß das Objekt ist auf dass er zeigt, nämlich sizeof(*p). Und das kann eben falsch sein. Einen dynamischen Typen gibt es hier nicht wirklich. Auch wäre es sehr dumm wenn für ein ++p; plötzlich irgendwelche virtuellen methoden aufgerufen werden müssten.



  • Shade Of Mine schrieb:

    moagnus schrieb:

    Dem Einwand von Simon2 stimme ich zu. Ich dachte nur, dass bei

    Base *bptr = new Derived;
    

    der Zeiger ja auch "weiß", wie groß der Speicherbereich ist, auf den er verweist.

    Dann frag ihn mal:
    sizeof(*bptr) wird dir sizeof(Base) und nicht sizeof(Derived) liefern....

    Mach ich (wenn auch implizit):

    delete bptr;
    

    ... und siehe da: Irgendwie "weiß" der Zeiger es doch.
    :p

    Mich stört das Ganze nicht so, weil ich Zeigerarithmetik fast genauso sehr scheue wie Makros/Defines (wenn auch weniger als goto), aber so gaaaanz zwingend scheint mir das nicht zu sein.

    Gruß,

    Simon2.



  • Simon2 schrieb:

    Mach ich (wenn auch implizit):

    delete bptr;
    

    ... und siehe da: Irgendwie "weiß" der Zeiger es doch.
    :p

    Ne, der Zeiger weiss da ja garnichts.
    Du rufst den Destruktor auf, wenn der nicht virtuell ist, dann machts plötzlich *PENG*. Und den Speicher gibt delete dann selber frei und da wird einfach die Adresse genommen und nachgeschaut wieviel speicher auf dieser Stelle reserviert wurde.

    void* p=::operator new(100);
    new(p) Dervied();
    operator delete(p);
    //DTor aufruf fehlt
    

    Hier tut delete eben exakt die 100 Byte freigeben.

    Mich stört das Ganze nicht so, weil ich Zeigerarithmetik fast genauso sehr scheue wie Makros/Defines (wenn auch weniger als goto), aber so gaaaanz zwingend scheint mir das nicht zu sein.

    Kapier ich nicht.

    Tools sind da um eingesetzt zu werden.



  • Shade Of Mine schrieb:

    Simon2 schrieb:

    Mach ich (wenn auch implizit):

    delete bptr;
    

    ... und siehe da: Irgendwie "weiß" der Zeiger es doch.
    :p

    Ne, der Zeiger weiss da ja garnichts.
    Du rufst den Destruktor auf ...

    Mir gings eigentlich gar nicht um den Destruktor, sondern um die Speicherfreigabe der Runtime.
    Aber selbst der virtuelle Destruktor zeigt doch, dass mit dem vermeintlich "nackten Zeiger" durchaus noch "Typinformation" verbunden ist. In diesem Fall ist sie z.B. über eine vtable implementiert - aber Implementierungsdetails sind eigentlich gar nicht die Frage.

    Eine andere Technik ist:

    Shade Of Mine schrieb:

    ...wird einfach die Adresse genommen und nachgeschaut wieviel speicher auf dieser Stelle reserviert wurde...

    Shade Of Mine schrieb:

    Mich stört das Ganze nicht so, weil ich Zeigerarithmetik fast genauso sehr scheue wie Makros/Defines (wenn auch weniger als goto), aber so gaaaanz zwingend scheint mir das nicht zu sein.

    Kapier ich nicht....

    OK - war auch ein wenig kurz. Hier nochmal länger:
    Mit Deiner pauschalen Aussage

    Shade Of Mine schrieb:

    ...Woher soll das Array denn wissen wie groß ein Objekt ist? ...

    lässt Du es so aussehen, als gäbe es schon prinzipiell gar keine Möglichkeit, über einen Zeiger an Informationen über das dahinterliegende Objekt zu kommen (z.B. Größe). Diese Aussage empfinde ich als zu pauschal. IMHO legt bereits jetzt die Runtime auch für einfache Zeiger "Typinformationen" ab (Beispiele: Speicherfreigabe, vtable aber letztlich auch RTTI). Es mag gute Gründe gegeben haben, warum diese Informationen für Zeigerarithmetik nicht herangezogen werden, aber dass es nicht anders ginge, ".... scheint mir nicht so ganz zwingend zu sein".

    Shade Of Mine schrieb:

    ...Tools sind da um eingesetzt zu werden.

    hmmmm - ich würde etwas stärker differenzieren: Tools sind da, um optimal zur Lösung passender Aufgaben eingesetzt zu werden.

    Ich habe nur sehr selten Aufgaben zu bewältigen, für die Zeigerarithmetik das optimale Werkzeug darstellt - das wollte ich damit ausdrücken.

    Gruß,

    Simon2.



  • Simon2 schrieb:

    Aber selbst der virtuelle Destruktor zeigt doch, dass mit dem vermeintlich "nackten Zeiger" durchaus noch "Typinformation" verbunden ist. In diesem Fall ist sie z.B. über eine vtable implementiert - aber Implementierungsdetails sind eigentlich gar nicht die Frage.

    Ja, über 2 indirektionen. Das ist bei O(1) operationen aber witzlos.

    lässt Du es so aussehen, als gäbe es schon prinzipiell gar keine Möglichkeit, über einen Zeiger an Informationen über das dahinterliegende Objekt zu kommen (z.B. Größe).

    Größe zB bekommt man garnicht. Die vtable beschreibt das Objekt, nicht den Speicherbereich den es belegt.

    Ein array kann nicht wissen wieviel es bei einem +1 weitergehen muss. Deshalb nimmt es den statischen Typ als Grundlage.

    Bedenke, dass Objekte nur auf rohem Speicher liegen - den Speicher kann ich beliebig beschaffen.



  • @Shade: Danke dir für die Erklärungen. 🙂
    Kann man also so zusammenfassen, dass Polymorphie nur dann in Aktion tritt, wenn über den Zeiger auf das Objekt zugegriffen wird (bspw. über eine Methode des Objekts)? Wenn dagegen der Zeigerwert verändert wird, wird der deklarierte Typ des Zeigers benutzt. Anders gehts auch nicht, weil ein Zeiger letztendlich auch nur eine Ganzzahl ist, die eine Speicheradresse speichert.


Log in to reply