 |
C++Talk.NET C++ language newsgroups
|
| View previous topic :: View next topic |
| Author |
Message |
Robert Semmering Guest
|
Posted: Wed Feb 09, 2005 9:42 am Post subject: Implementierung von Mehrfachvererbung |
|
|
Hallo.
Ich habe schon vieles einfach durch Durchdenken herausgefunden. Doch was
mir bis heute verschlossen blieb ist die Mehrfachvererbung in C++. Wer
kann mir helfen?
Zunächst was ich weiß:
Klassen definieren in C++ die Reihenfolge, wie Variablen im Speicher
(als Objekte) abgelegt werden. Daher kann der Compiler in den Kode
konstante Referenzen hineinschreiben um eine Elementvariable zu
referenzieren. Bei Einfachvererbung (B erbt von A) muss erreicht werden,
dass ein Objekt b der Klasse B genauso ansprechbar ist, wie ein Objekt a
der Klasse A. Also "B ist ein A". Die Anordnung im Arbeitsspeicher ist
also dieselbe, und die zusätzlichen Variablen von B kommen einfach
dahinter. So kann vom Compiler generierter Kode (aus dem statischen Typ
A) sowohl auf Speicherabbilder von A-, wie auf B-Objekte zugreifen.
Doch was ist bei einer Klasse AC die von A und C erbt?
Wie kann man sowohl mit Variablen vom Typ A wie auch vom Typ C auf
AC-Objekte zugreifen, wenn doch der Compiler für beide die gleichen
offsets verwenden will?
Wie sind Mehrfachvererbte Objekte im Speicher angeordnet?
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Stefan Reuther Guest
|
Posted: Wed Feb 09, 2005 9:00 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Robert Semmering wrote:
| Quote: | Zunächst was ich weiß:
Klassen definieren in C++ die Reihenfolge, wie Variablen im Speicher
(als Objekte) abgelegt werden.
|
(JFTR: falls du damit ausdrücken willst, dass die Objekte in der
Reihenfolge im Speicher stehen, wie sie in der Klassendeklaration
stehen, das muss nicht sein. Der Compiler darf umsortieren, solange
Variablen, die nicht durch einen access-specifier getrennt sind, ihre
Reihenfolge behalten, §9.2p12).
| Quote: | Daher kann der Compiler in den Kode
konstante Referenzen hineinschreiben um eine Elementvariable zu
referenzieren. Bei Einfachvererbung (B erbt von A) muss erreicht werden,
dass ein Objekt b der Klasse B genauso ansprechbar ist, wie ein Objekt a
der Klasse A. Also "B ist ein A". Die Anordnung im Arbeitsspeicher ist
also dieselbe, und die zusätzlichen Variablen von B kommen einfach
dahinter.
|
Das ist die übliche Implementierung, auch wenn sie (leider) nicht
garantiert ist.
| Quote: | Doch was ist bei einer Klasse AC die von A und C erbt?
Wie kann man sowohl mit Variablen vom Typ A wie auch vom Typ C auf
AC-Objekte zugreifen, wenn doch der Compiler für beide die gleichen
offsets verwenden will?
|
Hintereinander.
Du gehst vermutlich implizit davon aus, dass der Compiler mit dem
gleichen Zeiger auf die Objekte zugreift. Das ist nicht der Fall. Der
Compiler rechnet die Zeiger um. Wenn du ein AC in ein C castest,
erhältst du den Zeiger auf das C-Subobjekt. Beispiel:
struct A { int i, j; };
struct C { double d, e; };
struct AC : A, C { char c; };
void foo(AC* ac) {
A* a = ac;
C* c = ac;
std::cout << ac << "n" << a << "n" << c << "n";
};
Gibt bei mir aus:
0012FF6C
0012FF6C
0012FF74
Im Speicher sieht das (hier) so aus:
ac -------> .---. -. -.
a | i | | |
:---: | Typ A |
| Quote: | j | | |
c ----> :---: -: |
| | |
d | | |
| | | Typ AC
:---: | Typ C |
| | |
e | | |
| | |
:---: -' |
c | |
`---' -' |
Deswegen darfst du z.B. niemals sowas schreiben:
Base* b = new Derived();
void* tmp = b;
/* "wir wissen ja, dass es ein Derived ist und
casten es gleich richtig" */
Derived* d = static_cast<Derived*>(tmp);
FALSCH! Richtig ist
Derived* d = static_cast<Derived*>(static_cast<Base*>(tmp));
Ste"Jave ist toll"fan
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Robert Semmering Guest
|
Posted: Thu Feb 10, 2005 2:32 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
| Quote: |
Im Speicher sieht das (hier) so aus:
ac -------> .---. -. -.
a | i | | |
:---: | Typ A |
| j | | |
c ----> :---: -: |
| | | |
| d | | |
| | | | Typ AC
:---: | Typ C |
| | | |
| e | | |
| | | |
:---: -' |
| c | |
`---' -'
Deswegen darfst du z.B. niemals sowas schreiben:
Base* b = new Derived();
void* tmp = b;
/* "wir wissen ja, dass es ein Derived ist und
casten es gleich richtig" */
Derived* d = static_cast<Derived*>(tmp);
FALSCH! Richtig ist
Derived* d = static_cast<Derived*>(static_cast<Base*>(tmp));
|
Danke, das war schon mal sehr erleuchtend.
Aber es wirft natürlich noch mehr Fragen auf. Besonders der letzte Teil.
In Konsequenz bedeutet das für mich, das static_cast mir eventuell eine
andere Adresse zurückliefert, als hineingekommen ist ?
Um also beim obigen Beispiel zu bleiben, was passiert hier:
AC *ac = new AC();
A *a = ac; // Die Adresse in a und ac sind gleich ?
C *c = ac; // Die Adresse in c müsste jetzt ac+sizeof(a) sein?
Kann das sein?
Denn hier wird es ganz Problematisch.
delete c; // falsche Adresse -> Heap meldet sich
Aber das darf man ja per se wohl nicht machen, da dort die falschen
Destuktoren /falsche Reihenfolge dran kommt
| Quote: |
Ste"Jave ist toll"fan
|
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Stefan Reuther Guest
|
Posted: Thu Feb 10, 2005 7:08 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Robert Semmering wrote:
| Quote: | In Konsequenz bedeutet das für mich, das static_cast mir eventuell eine
andere Adresse zurückliefert, als hineingekommen ist ?
|
Definiere 'andere Adresse'.
Wenn du die Bitmuster vergleichst, oder die 'void*', dann ja.
Wenn du die Zeiger normal vergleichst
AC* ac = new AC();
C* c = ac;
AC* nac = static_cast<AC*>(c);
assert(c == nac);
assert(ac == nac);
dann natürlich nicht, denn hier wird für den Vergleich nac wieder in
einen C* gewandelt.
| Quote: | Um also beim obigen Beispiel zu bleiben, was passiert hier:
AC *ac = new AC();
A *a = ac; // Die Adresse in a und ac sind gleich ?
C *c = ac; // Die Adresse in c müsste jetzt ac+sizeof(a) sein?
Kann das sein?
|
Sinngemäß. Dem Compiler ist quasi komplett freigestellt, wie er
Basisklassen anordnet. Oftmals wird eine Anordnung gewählt, wo
'(char*)c == (char*)ac + sizeof(a)' gilt. Das muss aber nicht sein. Da
können noch vtbl-Zeiger im Weg liegen. Und das klassische Gegenbeispiel
ist die 'empty base class optimisation', wo eine leere Basisklasse
komplett wegoptimiert wird:
struct A { };
struct C { int i; };
struct AC : A, C { };
Hier gilt auf vielen Compilern sizeof(C) = sizeof(AC), und damit
sizeof(A) + sizeof(C) > sizeof(AC).
| Quote: | Denn hier wird es ganz Problematisch.
delete c; // falsche Adresse -> Heap meldet sich
Aber das darf man ja per se wohl nicht machen, da dort die falschen
Destuktoren /falsche Reihenfolge dran kommt
|
Natürlich. Wenn C keinen virtuellen Destruktor hat, darfst du genau das
nicht tun.
Wenn C jedoch einen virtuellen dtor hat, dann wird dort als erstes der
originale this-Pointer (ac) wiederhergestellt und das Objekt korrekt
abgeräumt.
Stefan
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Robert Semmering Guest
|
Posted: Fri Feb 11, 2005 6:20 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Stefan Reuther schrieb:
| Quote: | Robert Semmering wrote:
In Konsequenz bedeutet das für mich, das static_cast mir eventuell eine
andere Adresse zurückliefert, als hineingekommen ist ?
Definiere 'andere Adresse'.
Wie richtig erkannt: +sizeof(a oder wasauchimmer) |
| Quote: | Denn hier wird es ganz Problematisch.
delete c; // falsche Adresse -> Heap meldet sich
Aber das darf man ja per se wohl nicht machen, da dort die falschen
Destuktoren /falsche Reihenfolge dran kommt
Natürlich. Wenn C keinen virtuellen Destruktor hat, darfst du genau das
nicht tun.
Wenn C jedoch einen virtuellen dtor hat, dann wird dort als erstes der
originale this-Pointer (ac) wiederhergestellt und das Objekt korrekt
abgeräumt.
|
Heisst also dass delete sich die Adrese 'richtigrechnet' bzw auf den
passenden Typ castet. Sehr komplex das Ganze. Irgendwie kann ich mir
schon nicht mehr wirklich vorstellen, dass das alles zur
Übersetzungszeit geschehen kann. Aber RTTI braucht man wohl nicht, denn
Mehrfachvererbung gab es ja schon bevor es RTTI gab.
Was macht delete in so einem Fall?
(dynamischer Typ ist Ableitung inkl. Mehrfachvererbung)
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Jonathan Sauer Guest
|
Posted: Fri Feb 11, 2005 9:29 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Robert Semmering <robert.koepferl (AT) sonorys (DOT) at> wrote:
| Quote: | [...]
Wenn C jedoch einen virtuellen dtor hat, dann wird dort als erstes der
originale this-Pointer (ac) wiederhergestellt und das Objekt korrekt
abgeräumt.
Heisst also dass delete sich die Adrese 'richtigrechnet' bzw auf den
passenden Typ castet. Sehr komplex das Ganze. Irgendwie kann ich mir
schon nicht mehr wirklich vorstellen, dass das alles zur
Übersetzungszeit geschehen kann.
|
Jein. Der Compiler, den ich hier verwende (CodeWarrior Pro 4), geht
folgendermaßen vor:
Bei einem "delete Foo" wird der Destruktor von Foo aufgerufen und
bekommt als Parameter mit, daß er doch bitte sein delete aufrufen soll
(andere mögliche Parameter sind "einfach nur zerstören, ohne delete").
Dies ist nebenbei bemerkt auch nötig, damit "new" und "delete" überladen
werden können.
Da der Destruktor virtuell ist, geschieht der Aufruf über eine VTable,
in der der Compiler die polymorphen Methoden einer bestimmten Klasse
ablegt (Element 0 = foo, Element 1 = bar etc). Jedes Objekt hat einen
Zeiger auf die passende VTable, und wenn eine polymorphe Methode
aufgerufen werden soll, wird aus der VTable die Adresse herausgesucht
und dorthin gesprungen.
Klassen, die von mehreren Basisklassen erben, haben für jede Basisklasse
eine VTable. Im Objekt hat der Zeiger auf jede VTable die Position, die
der Zeiger in einer Instanz der jeweiligen Basisklasse hätte (der Zeiger
ist nichts weiter als ein verstecktes Attribut).
In der VTable ist nun nicht der Destruktor direkt abgelegt, sondern eine
vom Compiler generierte Methode, die -- sofern notwendig -- die Adresse
richtigrechnet und anschließend den echten Destruktor aufruft. Damit hat
der Destruktor die richtige Adresse und das delete der Klasse (oder das
globale delete) ebenfalls.
Dasselbe passiert auch mit normalen virtuellen Methoden.
Also quasi:
struct A {
VTable* vtableA;
int memberA1;
...
};
struct B {
VTable* vtableB;
int memberB1;
...
};
C erbt jetzt von A und B:
struct C {
VTable* vtableA;
int memberA1;
...
VTable* vtableB;
int memberB1;
...
};
vtableB von C enthält jetzt nicht die Methoden direkt, sondern die
Methoden, die die Adresse richtigrechnen (namentlich von "this" die
Größe von A abziehen) und anschließend die richtigen Methoden aufrufen.
| Quote: | Aber RTTI braucht man wohl nicht, denn Mehrfachvererbung gab es ja schon
bevor es RTTI gab.
|
ACK. Und Mehrfachvererbung funktioniert auch, wenn RTTI abgeschaltet
ist.
Gruß, Jonathan
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Stefan Reuther Guest
|
Posted: Fri Feb 11, 2005 10:47 pm Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Robert Semmering wrote:
| Quote: | Stefan Reuther schrieb:
Wenn C jedoch einen virtuellen dtor hat, dann wird dort als erstes der
originale this-Pointer (ac) wiederhergestellt und das Objekt korrekt
abgeräumt.
Heisst also dass delete sich die Adrese 'richtigrechnet' bzw auf den
passenden Typ castet. Sehr komplex das Ganze. Irgendwie kann ich mir
schon nicht mehr wirklich vorstellen, dass das alles zur
Übersetzungszeit geschehen kann. Aber RTTI braucht man wohl nicht, denn
Mehrfachvererbung gab es ja schon bevor es RTTI gab.
Was macht delete in so einem Fall?
(dynamischer Typ ist Ableitung inkl. Mehrfachvererbung)
|
Eine Möglichkeit ist, in den vtbls nicht nur die Adressen der virtuellen
Funktionen abzulegen, sondern auch noch die Offsets für die Korrektur
der this-Pointer. Anstatt "klassisch" einfach (Pseudocode)
this->vptr->dtor(this);
aufzurufen, ruft man
this->vptr->dtor((A*)((char*) this + this->vptr->dtor_offset));
auf. 'delete' verwendet dann ebenfalls dieses dtor_offset-Feld, um den
originalen Zeiger zu rekonstruieren.
Mal ein Beispiel:
struct A { virtual void x(); virtual ~A(); int e; };
ergibt beispielsweise folgendes Layout und folgende vtbl:
Objekt vtbl
.-------. .----------.
| Quote: | vptr |------->| A: |
:-------: | Offset 0 |
e | :----------:
`-------' | A::~A |
Offset 0 |
`----------' |
Entsprechend ergibt
struct C { virtual ~C(); virtual void y(), void z(); double d; };
dieses Aussehen:
.-------. .----------.
| Quote: | vptr |------->| C::~C |
:-------: | Offset 0 |
| :----------:
d | | C::y |
| | Offset 0 |
`-------' :----------:
C::z |
Offset 0 |
`----------' |
"Offset 0" heißt, dass keine Korrektur des this-Pointers nötig ist. Ist
ja auch logisch, der 'C'-Zeiger zeigt direkt auf den Anfang des Objektes.
Für
struct AC : A, C { void y(); };
ergibt sich nun aber folgendes. Da wir von zwei Klassen erben, hat das
Objekt zwei vtbls:
.-------. .----------.
| Quote: | vptr A |------->| A: |
:-------: | Offset 0 |
e | :----------:
:-------: | AC::~AC |
vptr C |-. | Offset 0 |
:-------: `----------'
| `.
d | .----------.
| `>| AC::~AC |
`-------' | Offset -8| |
:----------:
| Quote: | AC::y |
Offset -8|
:----------:
C::z |
Offset 0 |
`----------' |
Für die von A geerbten Dinge ändert sich hier nichts, da A am Anfang des
AC steht. Nur die neue Adresse des dtors ist in die vtbl eingetragen worden.
Bei C sieht das interessanter aus: um mit einem C-Zeiger den dtor von AC
aufzurufen, muss man die Adresse um 8 korrigieren, daher das 'Offset
-8'. Dadurch erhält man einen AC-Zeiger, mit dem man diese Funktion
aufrufen kann. Gleiches gilt für die in AC implementierte Methode y. Die
Methode z wird geerbt, es wird weiter die alte C-Version aufgerufen,
dafür ist dann keine Korrektur mehr nötig. Da jede Klasse einen eigenen
Destruktor hat, tritt das Problem, dass da eine geerbte Funktion steht,
nicht auf, so dass beim Destruktor immer die Offsetkorrektur steht, mit
der man aus dem 'Derived'-Objekt ein 'Base'-Objekt macht.
Das war jetzt eine Möglichkeit, das zu implementieren. Real existierende
Compiler werden vermutlich im Detail andere Formate wählen.
Diese Offsetkorrektur ist auch ein Grund, warum Mehrfachvererbung von
manchen Puristen gemieden wird. Es ist ein Feature, welches man bezahlt,
auch wenn man es nicht nutzt; auch, wenn man nur Einfachvererbung
verwendet, muss der Compiler die Offsetkorrektur vorsehen, falls
irgendwann mal jemand Mehrfachvererbung verwendet.
Man kann das ganze aber auch ohne diesen Overhead implementieren, so
dass in den vtbls wieder nur die Funktionsadressen stehen. In der C-vtbl
von AC würde dann eine Funktion stehen, die den C-Zeiger in einen
AC-Zeiger umwandelt und die eigentliche Funktion aufruft. Ich weiß
allerdings nicht, ob das jemand macht.
Stefan
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Thomas Mang Guest
|
Posted: Sat Feb 12, 2005 8:52 am Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
"Stefan Reuther" <stefan.news (AT) arcor (DOT) de> schrieb im Newsbeitrag
news:cujg6s.17k.1 (AT) stefan (DOT) msgid.phost.de...
| Quote: | Man kann das ganze aber auch ohne diesen Overhead implementieren, so
dass in den vtbls wieder nur die Funktionsadressen stehen. In der C-vtbl
von AC würde dann eine Funktion stehen, die den C-Zeiger in einen
AC-Zeiger umwandelt und die eigentliche Funktion aufruft. Ich weiß
allerdings nicht, ob das jemand macht.
|
Das ist die "thunk" - Implementierung, und ich dachte eigentlich die meisten
machen es so. Habe aber keinerlei Zahlen dafür.
Thomas
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
Stefan Reuther Guest
|
Posted: Sat Feb 12, 2005 11:45 am Post subject: Re: Implementierung von Mehrfachvererbung |
|
|
Thomas Mang wrote:
| Quote: | "Stefan Reuther" <stefan.news (AT) arcor (DOT) de> schrieb im Newsbeitrag
Man kann das ganze aber auch ohne diesen Overhead implementieren, so
dass in den vtbls wieder nur die Funktionsadressen stehen. In der C-vtbl
von AC würde dann eine Funktion stehen, die den C-Zeiger in einen
AC-Zeiger umwandelt und die eigentliche Funktion aufruft. Ich weiß
allerdings nicht, ob das jemand macht.
Das ist die "thunk" - Implementierung, und ich dachte eigentlich die meisten
machen es so. Habe aber keinerlei Zahlen dafür.
|
Gerade noch mal nachgeschaut:
Der gcc-2.95.2 verwendet die Implementation mit den Offsets in der vtbl.
Das war die Compilerversion, die ich mir intensiver angeschaut hab, weil
ich die doch noch recht häufig nutze.
Der gcc-3.4.1 verwendet standardmäßig die thunk-Implementierung. Es war
mir nicht bewusst, dass das neue ABI gleich solche Änderungen bringt :)
Stefan
--
de.comp.lang.iso-c++ - Moderation: mailto:voyager+mod (AT) bud (DOT) prima.de
FAQ: http://www.voyager.prima.de/cpp/ mailto:voyager+send-faq (AT) bud (DOT) prima.de
|
|
| Back to top |
|
 |
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
|