Effiziente Polymorphie

Wenn der Compiler eine Variable in eine Konstante umwandelt ist der resultierende Code noch immer dem Standard entsprechend, wenn er jedoch eine virtuelle Funktion in einer Kindklasse wie eine nicht-virtuelle behandelt ist das ein Verstoss gegen den §10.3, absatz 2:
Das ist mir soweit klar.
Nur frage ich mich, warum der Compiler inline ignorieren kann, wenn er das Inlining dieser Funktion für dumm hält (7.1.2 §2), virtual aber nicht:
A function declaration (8.3.5, 9.3, 11.3) with an inline specifier declares an inline function. The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism. An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions defined by 7.1.2 shall still be respected.

Eigentlich könnte dasselbe doch für alle anderen Keywords gelten, oder? Zum Beispiel, wenn nie ein Base* angefragt wird, kann der Compiler ja ziemlich sicher sein, dass Derived nicht virtual sein müsste.

Gruss
cwriter
 
Nochmal: Dass virtual vom Verhalten her auch in der Kindklasse gilt
ist so vorgeschrieben. Das muss keinen Sinn machen, es gilt auch so.

Und zu den VTable-Pointern:
Wie gesagt ist ein Grund, dass die einzelnen Binärteile vom Programm kompatibel bleiben.
Beschränkt auf eine CPP-Datei könnte der Compiler evt. schon unnötige Pointer wegoptimieren,
aber bei anderen CPP-Teilen vom gleichen Programm und/oder Libraries könnte die Entscheidung
anders ausfallen, und das ist zu dem Zeitpunkt noch nicht bekannt
(kann gar nicht sein. Man kann ja beim fertigen Programm später eine externe Lib
durch eine neue Kompilierung von leicht geändertem Code austaschen).
Wenn jetzt jede Kompilierungseinheit anders aufgebaute Klassenobjekte hat
können die einzelnen Teile nicht mehr zusammenarbeiten.

Zu inline:
Im Gegensatz zu virtual hat das keine Auswirkung auf andere Kompilierungseinheiten.
Es gibt eine Funktion, die wird in der selben CPP-Datei ein paar mal aufgerufen, und bei manchen dieser Aufrufe
kann es sinnvoll sein, den Inhalt der Funktion einfach dort direkt einzufügen, statt die ganze Funktion aufzurufen.
Gut, kann man machen. Die Funktion selber ändert sich dadurch kein bisschen, und wenn sie von anderen
Komp.einheiten (auch) aufgerufen wird, stört das nicht.

Warum der Compiler es ignorieren darf, virtual aber nicht:
Virtual/nichtvirtual kann je nach Code ein anderes nach außen sichtbares Programmverhalten erzeugen.
Eine Funktion inzulinen oder wirklich aufzurufen eben nicht. Das Programm wird immer das Selbe tun,
nur evt. ein paar Nanosekunden langsamer/schneller.
 
Zuletzt bearbeitet:
Äh. Ich schiebe die Schuld mal auf die Übermüdung...
Ich meinte natürlich nicht, virtual direkt zu streichen, sondern bei den Kindklassen explizite virtual zu verlangen, also zwischen virtual, (override) und virtual override zu unterscheiden.

Beschränkt auf eine CPP-Datei könnte der Compiler evt. schon unnötige Pointer wegoptimieren
Ja. Darf er aber nicht.

aber bei anderen CPP-Teilen vom gleichen Programm und/oder Libraries könnte die Entscheidung
anders ausfallen, und das ist zu dem Zeitpunkt noch nicht bekannt
(kann gar nicht sein. Man kann ja beim fertigen Programm später eine externe Lib
durch eine neue Kompilierung von leicht geändertem Code austaschen).
Dann hat man aber wieder den Header. Ein Beispiel:
C++:
//in lib.h
class internal_class{
virtual int get() {return 2+1;} //eine operation auf niedrigem level
};
class output_class : internal_class{
int get() override{return 3;} //eine Operation, die ein "schönes", gewrapptes Objekt zurückgibt.
}

//app.h
output_class oc;
oc.get(); //verarbeiten
Und dann innerhalb der lib eine Funktion, die die interne Klasse an die Ausgabeklasse vererbt. Die Funktion ist ja schon zur Compiletime in der lib bekannt, etwa "Nimm die interne Klasse und klatsche die andere Funktion drüber".
Es kann gut sein, dass das Hauptprogramm gar nicht erben will. Sollte das Hauptprogramm erben wollen, könnte es das ja noch immer tun, indem man die output_class nochmals mit virtual deklariert (bzw. die Memberfunktionen).

Dass virtual vom Verhalten her auch in der Kindklasse gilt
ist so vorgeschrieben. Das muss keinen Sinn machen, es gilt auch so.
Scheint so ;)

Gruss
cwriter
 
Ja. Darf er aber nicht.
Genau.

Und ... irgendwie scheint mir, du bringst das virtual-Verhalten
und das Vorhandensein eines Pointers durcheinander.

Abgeändertes Beispiel:
C++:
class A { public: virtual int get() {return 1;} };
class B : public A { int get() {return 2;} };
class C : public B { int get() {return 3;} };
...
B *x = new C();
int i = x->get();
delete x;
i ist 3, ganz einfach weil festgelegt wurde, dass ein virtual im Parent auch weiter unten gilt.
Ganz unabhängig von Pointern oder ob irgendwo vererbt wird oder nicht, im Std.
vorgeschrieben ist für sowas nur, was das Ergebnis zu sein hat.

Deswegen müsste aber theoretisch nicht jedes Objekt einen Pointer dabeihaben.
Warum der aber trotzdem nötig ist ist eben zB. die Sache mit der Kompaitibiltät zwischen mehreren CPP-Dateien.
In deinem Beispiel
* bräuchten die Objekte bei app.cpp (app.h?) und lib.h keinen VTable-Pointer.
* die Objekte bei lib.cpp und lib.h evt. auch nicht.
* Eine dritte CPP-Datei, die jetzt noch nicht einmal existieren muss, brauchts evt. schon.
Und solange der Pointer nur irgendwo nötig ist muss er überall sein, weil man sonst zB. keine Objekte aus
einer Cpp-Datei als Paramener zu Funktionen einer anderen Cpp-Datei übergeben kann.
 
Zuletzt bearbeitet:
Nein, ist es nicht :)
Es ist i=2, da eine Instanz von B auf den Typ B gelegt wurde. Bei B* b = new C; bin ich mir nicht sicher... Ich rate mal, dass die Ausgabe dann 3 ist (Wenn es überhaupt kompiliert). Ich kann es gerade nicht testen :(

Ich drehe mal um: Kann man zur Laufzeit vererben bzw. die VTable ändern?

Zur Compiletime kennt der Compiler den Funktionsinhalt, egal, ob virtual oder nicht. Er kann also die Funktionen direkt zuweisen.

Oder anders gesagt: Die VTable wird ja nur fürs Vererben gebraucht, nicht fürs Erben (ausser ich hätte eine total falsche Vorstellung von Vtables (die ist: eine Map Funktions-ID zu Funktionspointer).
Denn:
Vererbung: Die Elemente der Elternklasse müssen überschrieben werden können.
Erbung: Es müssen jediglich Elemente zur Erbenden Klasse hinzugefügt werden können.

Überschreibung erfordert virtual, Hinzufügung nicht.
Meine grösser-als und kleiner-als funktionieren nicht...
Denn wenn man Base* b = reinterpret_cast≤Base*≥(Derived*);b-≥Func(); macht, wird ja eigentlich nur Base::Func statt Derived::Func auf den gleichen Speicherbereich gewirkt, es wird nichts an den Klassen verändert.
* Eine dritte CPP-Datei, die jetzt noch nicht einmal existieren muss, brauchts evt. schon.
In welchem Fall?


Gruss
cwriter
 
Nein, ist es nicht
:o Wollte "new C()" schreiben :D

Ich rate mal, dass die Ausgabe dann 3 ist (Wenn es überhaupt kompiliert). Ich kann es gerade nicht testen
Es ist 3, ja (wenn es kompiliert :D Ein paar "public:" hatte ich vergessen)
Für so schnelle Tests ist zB. Ideone nützlich: http://ideone.com/Zq1E7Y

Ich drehe mal um: Kann man zur Laufzeit vererben bzw. die VTable ändern?
Bin mir nicht sicher, ob wir darunter das Gleiche verstehen, aber nein. Bzw. man kann
den Pointer im Speicher schon ändern (vorausgesetzt, das eigene OS verwendet überhaupt
Vpointer, müsste ja theoretisch auch nicht sein), aber ob dann alles in jedem Fall
funktioniert ist zu bezweifeln.

Zur Compiletime kennt der Compiler den Funktionsinhalt, egal, ob virtual oder nicht. Er kann also die Funktionen direkt zuweisen.
Eben nicht. Jede Cpp-Datei wird einzeln behandelt, und was in den
anderen Cpps (bzw. schon vorkompilierten Dingen) ist, ist unbekannt.

Oder anders gesagt: Die VTable wird ja nur fürs Vererben gebraucht, nicht fürs Erben
(ausser ich hätte eine total falsche Vorstellung von Vtables (die ist: eine Map Funktions-ID zu Funktionspointer).
Denn:
Vererbung: Die Elemente der Elternklasse müssen überschrieben werden können.
Erbung: Es müssen jediglich Elemente zur Erbenden Klasse hinzugefügt werden können.
...
Überschreibung erfordert virtual, Hinzufügung nicht.
Interessante Wortunterscheidung. Bisher hab ich beide Worte einfach für Beides verwendet :)
Und wieder nein, In KIndklasse können Funktionen hinzugefügt werden, die (diesmal
ausgehend von der Kindklasse) virtual sind. Dann wird die Table wieder relevant.

Ganz ohne erbende Klassen, nur mit einer Klasse allein, die was Virtuelles hat,
gibts schon Table und Pointer, weil ja später was vererbt werden könnte.

Aber ja, die Table hat pro virtueller Funktion eine Adresse, welche
tatsächliche Implementierung für das Objekt eben relevant ist.

Meine grösser-als und kleiner-als funktionieren nicht...
Denn wenn man
Code:
Base* b = reinterpret_cast≤Base*≥(Derived*);
b-≥Func();
macht, wird ja eigentlich nur Base::Func statt Derived::Func auf den gleichen Speicherbereich gewirkt, es wird nichts an den Klassen verändert.
Versteh ich leider nicht. (Codetags von mir, Code macht so aber keinen Sinn)

Was denn?
In welchem Fall deine Cpp-Datei das brauchen kann?
In welchem Fall es die Cpp-Datei noch nicht geben muss?
 
Bin mir nicht sicher, ob wir darunter das Gleiche verstehen, aber nein. Bzw. man kann
den Pointer im Speicher schon ändern (vorausgesetzt, das eigene OS verwendet überhaupt
Vpointer, müsste ja theoretisch auch nicht sein), aber ob dann alles in jedem Fall
funktioniert ist zu bezweifeln.
Genau so meine ich das :)
Eben nicht. Jede Cpp-Datei wird einzeln behandelt, und was in den
anderen Cpps (bzw. schon vorkompilierten Dingen) ist, ist unbekannt.
Wir meinen nicht dasselbe.
C++:
//Header
class A
{
    public:
        virtual void func();
        int m_i;
};

class B : public A
{
    public:
        void func() override;
};
//Source / CPP
void A::func()
{
    printf("Bla");
}

void B::func()
{
    printf("Blubber");
}
Wenn jetzt B in einer Funktion instanziiert wird (B b;), dann schaut der Compiler nach, was B ist. Er sieht "class B : public A" und sagt sich "Beginnen wir mal mit A". Er schaut nach, was A ist und sieht: "Aha, eine Funktion virtual void func() und eine Membervariable m_i". Dann geht er zurück zu B und sieht "Oha, da wird die Funktion void func() überschrieben, das m_i bleibt aber."
Soweit einverstanden?

Ich würde sagen, der Compiler könnte hier gefahrlos die VTable wegoptimieren (wenn der Standard entsprechend kein implizites virtual hat natürlich).
Er könnte schauen "Aha, B ist A mit einer anderen Funktion, die aber gleich heisst. Da das override selbst aber kein explizites virtual hat, kann ich das als nicht-virtuelle Klasse betrachten, quasi die komplette Definition von virtual void A::func() mit der kompletten Definition ('override' ist ja eigentlich keine Definition, sondern nur ein Garant, dass tatsächlich etwas überschrieben wird) von void B::func() überschreiben."

-> Der Compiler muss die Definition der Klassen und die Deklaration der Memberfunktionen kennen, sonst wird er einen Fehler werfen.

Er muss somit nicht die cpp-Dateien kennen, um die Klassen optimieren zu können. (Klar kann er nicht alle Klasseninstanzen überprüfen gehen, aber er kann an der Wurzel sehr leicht ansetzen).
Interessante Wortunterscheidung. Bisher hab ich beide Worte einfach für Beides verwendet :)
Und wieder nein, In KIndklasse können Funktionen hinzugefügt werden, die (diesmal
ausgehend von der Kindklasse) virtual sind. Dann wird die Table wieder relevant.
"Vererbt an" bzw. "erbt von" erklärt es vielleicht besser.
Wenn die Kindklasse aber virtuelle Funktionen hat, wird sie eine (potentielle) Elternklasse.
Wenn sie aber keine virtuelle Funktionen hat (und der Standard override nicht als implizites virtual gelten würde), dann ist sie sicher keine (potentielle) Elternklasse.

Oder als Beispiel:
Code:
//vv = virtual function
//oo = override (non-virtual)
//vo = virtual override
//ff = non-virtual function
// | = Trenner Klassenzugehörigkeit
Klasse A: (Muss virtual sein, kann nicht optimiert werden)
Funcs: vv vv vv vv
Data:  0 - 3

Klasse B : A: (Muss virtual sein, kann nicht optimiert werden)
Funcs: vv vo vv oo | ff ff
Data:  0 - 3 | 4 - 15

Klasse C : A: (Muss nicht virtual sein, kann optimiert werden)
Funcs: oo oo oo oo | ff ff
Data: 0 - 3 | 4 - 7

Ganz ohne erbende Klassen, nur mit einer Klasse allein, die was Virtuelles hat,
gibts schon Table und Pointer, weil ja später was vererbt werden könnte.
Einverstanden.

Versteh ich leider nicht. (Codetags von mir, Code macht so aber keinen Sinn)
Äh ups :)
Es wird natürlich Derived::Func ausgeführt (dank vTable) und mit den Daten aus Derived.

Was denn?
In welchem Fall deine Cpp-Datei das brauchen kann?
In welchem Fall es die Cpp-Datei noch nicht geben muss?
Ersteres.

Wobei ich gerade beginne, am virtual-Konzept zu zweifeln. Ein Beispiel:
Code:
Derived d;
00106FAA lea ecx,[d]  
00106FAD call Derived::Derived (0AEC6Eh)  //_vfptr (so heisst die vTable in VS) wird erstellt
Base* b = (&d);
00106FB2 lea eax,[d]  //Kopiert den Wert von d (_vfptr) nach EAX
00106FB5 mov dword ptr [b],eax  //Überschreibt den _vfptr von Base mit dem von Derived
b->Func();
00106FB8 mov eax,dword ptr [b] //Bewegt d in das eax-Register
00106FBB mov edx,dword ptr [eax]  //Bewegt __vfptr in das edx-Register
00106FBD mov esi,esp  
00106FBF mov ecx,dword ptr [b]  //Bewegt den _vfptr bzw. die Startadresse der Klasse von b nach ECX
00106FC2 mov eax,dword ptr [edx]  //Bewegt _vfptr[0] nach EAX
00106FC4 call eax  //Ruft _vfptr[0] auf (eax ist 0125C810h)
00106FC6 cmp esi,esp  //So'n Debug-Zeugs halt :P
00106FC8 call __RTC_CheckEsp (0AD350h) 
d.Func(); //Test
012B755D lea ecx,[d]  //Für die ganz Findigen: Ja, das ist aus 2 verschiedenen Builds zusammengeklebt.
012B7560 call Derived::Func (0125C810h) //Stimmt überein
Eine Klasse ohne _vfptr:
Code:
Normal n;
002D747D lea ecx,[n]  
002D7480 call Normal::Normal (027BD11h) 
n.Func();
002D7485 lea ecx,[n]  
002D7488 call Normal::Func (027DFEEh)

Soweit das, was geschieht. Nun mein Vorschlag: Anstatt jedem Objekt Ballast mitzugeben, könnte man doch einfach bei der Anweisung Base* b = &d; den Typ für künftige Operationen anpassen, i.e. alle virtual functions der Basisklasse durch die jeweiligen Overrides ersetzen. Also wenn die Klassen sind:
Code:
class Base {
public:
    virtual void Func(){ printf("Bla"); }
    virtual void Func2(){ }
};

class Derived : public Base {
public:
    void Func() override{ printf("Blubber"); }
};
Dann kann der Compiler ja folgendes generieren:
Code:
Derived d;
Base* b = &d;
b->Func();
//mov ecx, dword ptr [b]
//call Derived::Func()
b->Func2();
//call Base::Func2();
Denn ganz offensichtlich ist es sonst ja doppelt gemoppelt: Die Adresse wird in __vfptr und statisch gefunden.
Und der Compiler kennt a) die Basisklasse und b) die erbende Klasse und damit die Funktionsadressen.

Ausser, ich hätte wie so oft etwas übersehen - es muss ja schon einen Grund geben, warum es so gemacht wird; er erschliesst sich mir nur nicht so ganz.

Mir schwirrt langsam der Kopf. Ist wie beim langen Studieren von einem Wort: Irgendwann versteht man es nicht mehr...

Gruss
cwriter
 
All deine Ausführungen setzen voraus, dass die App Derived kennt. Eines der Grundkonzepte von Polymorphie ist ja eben, dass du nur mit Base arbeitest und überhaupt nicht weisst, dass es Derived gibt.

lib.h
C++:
class Foo {
public:
     virtual void bar() { }
     static std::shared_ptr<Foo> create();
};

lib.cpp
C++:
class Bar : public Foo {
     void bar() { /* .... */ }
};

std::shared_ptr<Foo> Foo::create() { return std::make_shared<Bar>(); }

app.cpp
C++:
auto foo = Foo::create();
foo->bar();
 
Ich habe deine Ausführungen mal durch den Compiler gejagt:
Code:
auto foo = Foo::create();
011C181E  call        Foo::create (011C107Dh) 
011C1823  mov         dword ptr [foo],eax 
    foo->bar();
011C1826  mov         eax,dword ptr [foo] 
011C1829  mov         edx,dword ptr [eax] 
011C182B  mov         esi,esp 
011C182D  mov         ecx,dword ptr [foo] 
011C1830  mov         eax,dword ptr [edx] 
011C1832  call        eax 
011C1834  cmp         esi,esp 
011C1836  call        __RTC_CheckEsp (011C116Dh) 
    return 0;
(Ich habe mir die Freiheit genommen, die ekelhaften smart pointer zu entfernen. Gibt mir immer ein bisschen zu viel Gemüse im Disassembly.)

Aber ja, hier kann der Compiler nichts wegoptimieren - danke fürs Beispiel und Erklären des __vfptr-Zwangs :)

Das erklärt aber wirklich nur den Zwang des __vfptr für die Basisklasse. Oder anders gesagt:

1) Hier ist dem Compiler Derived unbekannt und Base bekannt.
2) Den umgekehrten Fall, dass Derived bekannt ist und Base unbekannt, kann es nicht geben.
3) Dafür gibt es den Fall, dass Derived bekannt und Base bekannt sind.

Im Prinzip meinte ich sowas wie das C# sealed - ob nun implizit oder explizit. Jedoch müsste ja auch eine Klasse, die nicht vererbt, einen __vfptr haben, wie dein Beispiel zeigt. Es müsste also ein Sealed in 2 Richtungen sein - vererbt, aber statisch und daher nur bei 3) anwendbar (ja, ist ein bisschen kompliziert).

Allerdings gibt es doch Fälle, wo es praktisch wäre, wenn eine Klasse die Funktionsaufrufe direkt auflösen könnte (auch auf Wunsch) und nicht den Umweg über Laufzeitoperationen gehen müsste.

Z.B. sowas wie "intern" o.ä., denn man will ja nicht in jeder Polymorphie den Pointer weitergeben können. Und bei 10000 Objekten, die je 12 statt 8 Bytes gross sind und jedes Mal noch einen zusätzlichen Konstruktionsschritt brauchen, dürfte der __vfptr in der Performance doch auffallen - vor allem, wenn er eigentlich gar nicht nötig wäre.

Aber jede Klasse mit immer denselben Basiseigenschaften neuschreiben, um den __vfptr zu umgehen will man ja wahrscheinlich auch nicht...

Oder gibt's das alles schon und ich kenne es einfach nicht?

Gruss
cwriter
 
Es gibt grundsätzlich keinen Zwang eine VMT zu verwenden. Der Standard sagt ja nur aus, was passieren muss wenn man eine virtuelle Funktion aufruft, wie man das dann aber umsetzt ist jedem freigelassen. Bekommst du es ohne eine VMT als Member des Objekts hin, auch ok.
 
Zurück