Effiziente Polymorphie

cwriter

Erfahrenes Mitglied
Hallo Welt

Ich habe mich an einer kleinen Scriptengine versucht und bin soweit auch recht erfolgreich. Zwar sollte die dazugehörige Sprache soweit typesafe sein, dass es Typen gibt, diese aber bei Bedarf unter Ausgabe einer Warnung implizit umgewandelt werden können. Also zum Beispiel
Code:
string zahl = "5";
zahl = 3;
//zahl wird zu int, eine Warnung wird ausgegeben
//Um das klarzustellen: Natürlich werde ich daran denken, fromInt()-Ähnliche Funktionen bereitzustellen

Dazu wollte ich eine Klasse nutzen, die alle Typen darstellen kann. Dazu habe ich eine Klasse erstellt, die einen Member "int type" hat, der die verschiedenen Typen darstellt (string, int, float, etc.).
Die Daten (bzw. der Wert des Typs) werden in "void* data" gespeichert und je nach Typ wird anders gecastet und operiert.
Das Grundgerüst sieht also so aus:
C++:
class dynVar
{
    int type;
    void* data;
};
Und ja, ich habe den Speicher schon im Griff, auch mit Copy-Operations u.ä.
Was dem Leser jetzt wahrscheinlich schon durch den Kopf geht, ist: "Warum nicht als Basisklasse und Kindklassen realisieren?".

Diese Idee kam mir, nachdem ich schon einen Grossteil geschrieben hatte (ja, C lässt mich einfach nicht los :) ). Doch was ich mich fragte, war, ob es denn effizienter ist, mit Vererbung zu arbeiten. Dazu folgendes: Meine jetzige dynVar-Klasse umfasst 20 "normale" (also nicht-virtuelle) Funktionen sowie folgende Membervariabeln:
C++:
int type; //4 Bytes
void* data; //4 Bytes auf 32bit, 8 Bytes auf 64bit
size_t* m_reference_count; //4 Bytes auf 32bit, 8 Bytes auf 64bit
Also 12 Bytes (32bit) bis 20 Bytes (64bit). Der Compiler bestätigt mir mit sizeof() die erwarteten 12 Bytes.
Nun habe ich ein bisschen mit dem Compiler gespielt:
C++:
Test1 _t1;

_t1.test();
Test2 _t2;

_t2.test();
Test3 _t3;

_t3.test();
Test4 _t4;

_t4.test();

printf("Test1: %d\nTest2: %d\nTest3: %d\nTest4: %d\nTest5: %d\n\n", (int)sizeof(Test1), (int)sizeof(Test2), (int)sizeof(Test3), (int)sizeof(Test4), (int)sizeof(Test5));

Die Definitionen zu den Klassen:
C++:
class Test1 {
public:
    virtual void test() { printf("Test1"); }

};

class Test2 {
public:
    void test() { printf("Test2"); }

};

class Test3 : publicTest1{
public:
    void test() override { printf("Test3"); }

};

class Test4 {
public:
    virtual void test() { printf("Test4"); }
    virtual void test_2() { printf("Test4_2"); }

};

class Test5 {
public:
    virtual void test() = 0;
    virtual void test_2() = 0;

};

Bevor wir uns die Ausgabe anschauen: ASM :)
Code:
Test1 _t1;

0032A955 lea ecx,[_t1]

0032A958 call Test1::Test1 (029DBA1h)

_t1.test();

0032A95D lea ecx,[_t1]

0032A960 call Test1::test (029DB9Ch)

Test2 _t2;

_t2.test();

0032A965 lea ecx,[_t2]

0032A968 call Test2::test (029DBA6h)

Test3 _t3;

0032A96D lea ecx,[_t3]

0032A970 call Test3::Test3 (029DBB0h)

_t3.test();

0032A975 lea ecx,[_t3]

0032A978 call Test3::test (029DBABh)

Test4 _t4;

0032A97D lea ecx,[_t4]

0032A980 call Test4::Test4 (029DBB5h)

_t4.test();

0032A985 lea ecx,[_t4]

0032A988 call Test4::test (029DBBFh)

printf("Test1: %d\nTest2: %d\nTest3: %d\nTest4: %d\nTest5: %d\n\n", (int)sizeof(Test1), (int)sizeof(Test2), (int)sizeof(Test3), (int)sizeof(Test4), (int)sizeof(Test5));

Und schliesslich die Ausgabe:
Code:
Test1: 4
Test2: 1
Test3: 4
Test4: 4
Test5: 4

Das 1 kommt wohl nur daher, dass sizeof() nicht gerne 0 hat. Alles andere sagt hingegen folgendes aus:
* alle Klassen, die mindestens 1 "virtual"-Keyword haben, brauchen mindestens 4 Bytes.
* alle Klassen, die mindestens eine virtuelle Funktion überschreiben, brauchen mindestens 4 Bytes.
* alle Klassen, die mindestens eine virtuelle Funktion haben, erfordern einen (Default-) Konstruktoraufruf.
* alle Klassen, 1) die keine virtuelle Funktion beinhalten und
2) für die keine der ersten drei Punkte zutreffen und keine Klasse beinhalten erfordern keinen Konstruktoraufruf.
Fügt man bei class Test1 noch ein "int i;" ein, dann sind Test1 und Test3 jeweils 8 Bytes gross. Daher:
*Die oben genannten Grössen werden zu der Grösse der Variabeln addiert.

Das Verhalten konnte unter Visual Studio 2013 (v120) sowohl im Debug- als auch im Releasemode auf 32bit-Architektur beobachtet werden. Ohne es überprüft zu haben, rechne ich mit 8 statt 4 Bytes auf 64bit.

Auf meinen Anwendungsfall bezogen heisst das folgendes:
Eine Basisklasse mit virtuellen Funktionen und dem Reference Counter (Für Memory-Management): 4 + 4 = 8 Bytes (immer vorhanden).

Für Strings:
(Da ein std::string 28 Bytes gross ist, wird mit void* bzw. char* gearbeitet.)
* char* str: 4 Bytes
Total: 12 Bytes

Für Ints:
* int i: 4 Bytes
Total: 12 Bytes

(auf 64bit jeweils 8 + 8 + 8 bzw. 8 + 8 + 4 Bytes)(ungetestet)

Zudem:
* Der Konstruktor, wenn auch compilergeneriert, wird ausgeführt und weist die virtuellen Funktionen zu.

Fazit:
Man braucht eher noch mehr Ressourcen mit dem komplett-C++-Ansatz als mit dem C++-C-Mischmasch.

Nun zu meinen Fragen:
Das wichtigste zuerst: Wo habe ich, falls dem so ist, einen Fehler gemacht?
Falls keine Antwort auf die erste Frage (=wenn ich alles richtig interpretiert habe): Gibt es einen Grund, warum man (in diesem Beispiel) dennoch Vererbung benutzen sollte?
Gibt es einen Grund, warum man meinen Erstansatz NICHT verwenden sollte?
Konzeptbetreffend:
Vererbung scheint bei kleinen Klassen vergleichsweise ineffizient. Warum wird es dennoch so oft benutzt?

Ich bin mir sicher, ich hatte noch einige Fragen mehr. Diese fallen mir jetzt aber leider nicht mehr ein :(

Gruss
cwriter
 
Zuletzt bearbeitet:
Hi :)

Zuerst zu deinen Ausführungen zu virtual usw., später Fragen/Alternativen für das Scriptvariablenproblem:

int type; //4 Bytes
void* data; //4 Bytes auf 32bit, 8 Bytes auf 64bit
size_t* m_reference_count; //4 Bytes auf 32bit, 8 Bytes auf 64bit
...
(int)sizeof(Test1)
Genau genommen ist auch int nicht fix 4 Byte.

Warum ist m_reference_count ein Pointer?

sizeof liefert ein size_t, casten zu int kann theoretisch andere Ergebnisse bringen
(bei so großen Werten, wie man vermutlich nie von sizeof bekommen wird...)

Das 1 kommt wohl nur daher, dass sizeof() nicht gerne 0 hat.
Der C++-Standard verlangt, dass jedes Objekt seine eigene Adresse hat oder so ähnlich.
(wenn das nicht verlangt werden würde würden andere Sachen keinen Sinn machen)
In einem zB. Array von sizeof-0-Objekten hätte jedes die selbe Adresse.
Also verbraucht jedes Ding min. ein Byte, das der Compiler wenn nötig künstlich dazuerfindet.

* alle Klassen, die mindestens 1 "virtual"-Keyword haben, brauchen mindestens 4 Bytes.
* alle Klassen, die mindestens eine virtuelle Funktion überschreiben, brauchen mindestens 4 Bytes.
Virtuelle Funktionen führen dazu, dass es in den Objekten sog. Vtable-Pointer gibt; Pointer,
die zu den für sie gültigen Funktionen zeigen. Braucht eben eine Pointergöße (4,8,...).

Bzw. man kann (als C++-Compilerhersteller etc.) das Problem, die passende Funktion
zu ermitteln, auch auf andere Arten lösen, aber diese Vtablepointersache ist das
Übliche auf allen mir bekannten Systemen.

* alle Klassen, die mindestens eine virtuelle Funktion haben, erfordern einen (Default-) Konstruktoraufruf.
Nicht ganz sicher, was du damit meinst. Wenn du einen Konstruktor hast, der
Parameter nimmt, und den zum Objekterstellen verwendest, ist auch alles ok.
Einfach jeder Konstruktor, der von C++ erlaubt ist, passt. Was bei Objekterstellung
außer dem evt. eigenen Konstruktorinhalt gemacht wird ist eben die Befüllung der
Vtablepointer, das generiert der Compiler dazu.

alle Klassen, 1) die keine virtuelle Funktion beinhalten und
2) für die keine der ersten drei Punkte zutreffen und keine Klasse beinhalten erfordern keinen Konstruktoraufruf.
Nicht für die Vtablesache, ja. Und wenn man keinen eigenen Konstruktor
geschrieben hat (=der Konstruktor "leer" ist) braucht man auch dafür nichts.

Aber: Darauf kann man sich eig. nicht verlassen So ein Objekt mit malloc anzulegen und
dann zu verwenden, als wäre es "richtig" angelegt worden, ist laut Standard "undefined
behaviour", = alles mögliche kann passieren.

Wenn man einen malloc-ierten Speicher als Objekt verwenden will gibt es in C++ ein
sog. "Placement-new", um alle Anlegearbeit außer der Speicherreservierung zu machen.
Damit passts dann wieder.

Fügt man bei class Test1 noch ein "int i;" ein, dann sind Test1 und Test3 jeweils 8 Bytes gross. Daher:
*Die oben genannten Grössen werden zu der Grösse der Variabeln addiert.
Die Größe des Gesamtobjekts ist nicht unbedingt die Summe der Inhalte, kann auch größer sein,
abhängig davon, welche Variablenarten in welcher Reihenfolge in der Klasse sind (Alignment/Padding,
bei Bedarf genaueres dazu), und auch abhängig von Compilereinstellungen.

...ist leider nicht dafür bekannt, als Referenz geeignet zu sein. C99 (von 1999)
ist noch immer nicht vollständig implementiert, (auch in der neuesten Version
von VS nicht), und neue Sporachstandards sowieso nicht.


(Da ein std::string 28 Bytes gross ist, wird mit void* bzw. char* gearbeitet.)
Die Größe von string ist implementierungsabhängig.
Aber zwei Positivpunkte für string:
a) Die Poitnerlösung braucht nicht nur den Pointer und die Daten, sondern auch
Metadaten die beim OS fpr jede Allokierung anfallen. Dürften auch mehrere pointergröße Sachen sein.
b) Manche Stringimplementierungen sind so gemacht, dass kurze Strings bis zu einer bestimmten
Maximallänge ohne Heapallokierung direkt im Objekt gespeichert werden können (nur der
sizeof-Speicherverbrauch, und schnellerer Zugriff)

Anderes Thema: Hast du bei einer Scriptsprache an Charsets gedacht?
Codedatei allgemein bzw. Stringliterale...

Man braucht eher noch mehr Ressourcen mit dem komplett-C++-Ansatz als mit dem C++-C-Mischmasch.
Kann sein (manchmal auch nicht). Aber normalerweise hat man nicht Milliarden solcher Objekte,
also fällt es nicht so ins Gewicht, vergleichen zur Programmiererleichterung bei sinnvoller
Anweundung (zB. die Variablenanzahl in Scripten deiner Sprache wird wohl nicht so groß
werden, und Arrays können ja gesondert behandelt werden)

Das wichtigste zuerst: Wo habe ich, falls dem so ist, einen Fehler gemacht?
Groben Fehler seh ich bisher keinen.

Vererbung scheint bei kleinen Klassen vergleichsweise ineffizient. Warum wird es dennoch so oft benutzt?
Wie gesagt, Objektorientierung mit allem drum und dran macht das Programmieren oft einfacher,
gerade für größere Projekte, als prozedurales C. Die Vorteile überwiegen die zusätzlichen Pointer
im Speicher bei Weitem.

Gibt es einen Grund, warum man meinen Erstansatz NICHT verwenden sollte?
Naja ... weiß nicht :)
a) Aus Sicht vom Scriptenginenschreiber ists in vielen Punkten angenehm
b) Allerdings extra Klassen für jeden Grundtyp zu schreiben
b) Man braucht für jede einfache int.-Variable im Script relativ viel Speicher
(deine Objekte, Heapmanagement, Name)
c) da es nicht wirklich viel Variablen geben wird macht das nicht so viel.
...
denke, könnte schlimmer sein.

Etwas union-basiertes wäre eine andere Möglichkeit, deutlich weniger
Speicherbelastung wegen Heapvermeidung für einfache Variablen:
C++:
class variable
{
    int type; //int, float, intarray, floatarray, string...
    union //nur so groß wie der größte Teil drin, also hier Pointergröße
    {
        int i;
        float f;
        void *v;
    } data;
};
...
std::map<std::string, variable> variablen;

...
Wenn deine Scriptsprache sowas wie lokale Variablen mit automatischer
Lebensdauer und geschachteltem Scope hat, also (in C++-Art) sowas:
C++:
int main()
{
    int a; //a anfang
    {
        int b; //b anfang
        if(lala)
        {
             int c; //c anfang
        } //c ende
    } //b ende
} //a ende
dann könnte man (für diese Variablen, zusätzlich zur Hauptlösung) eine Art Stack machen.
Ein vorallokiertes Bytearray am Heap (zB. 1MB, mit "einer" OS-Anfrage), dazu eine Map
von Namen zu Byteoffset, und die Variablen drin werden eben "gestapelt".
a bekommt Byteoffset 0, bis zur Löschung von a muss es nicht mehr verschoben werden etc.
Stack ist 4 Byte voll. b von 4 weg (bis 7), c von 8 weg usw., und gelöscht wird eben umgekehrt.

"Alle" Variablen selber in einem Speicherblock managen ist dagegen wieder kontraproduktiv,
weil man nur die im OS eingebauten Sachen nachmacht, und das ja deutlich komplexer wird
(Anfragen beliebiger Größe, das Wegräumen ist nicht immer so schön in verkehrter Reihenfolge,
deshalb entstehen Löcher im verbrauchten Speicher, man braucht irgendwo einen Baum
um freie/belegte Teile zu finden...)
 
Genau genommen ist auch int nicht fix 4 Byte.
Es ist nicht standardisiert, aber soweit ich mich erinnere, ist int auf den meisten Systemen 4 Bytes gross. Die grossen Unklarheiten kommen meines Wissens erst bei "long", wo es dafür dann richtig abgeht, was Systemspezifisches abgeht.
Aber generell gilt bei den Byteangaben: Ich arbeite fast nur auf Windows 10 mit VS2015 Community und dem v120 Compiler (viele Bibliotheken wurden mit v130 aka 2015 ja völlig zerschossen, da das komplette System geändert wurde - danke Microsoft). Aber Windows 10 hat aus Programmiertechnischer Sicht viele Bugs, und diese sind hier ja auch nicht das Thema :)

Warum ist m_reference_count ein Pointer?
Das erfordert ein bisschen weiter gefasste Erklärungen: Der Plan ist, dass Variabelnkopien per se nur read-only sind und erst aktiviert werden müssen (erst später eine Deep Copy) gemacht wird. Somit wird im Copy-Constructor der Wert des ref counter erhöht und der Pointer dazu in die Kopie kopiert. Wird eine der beiden Instanzen dann gelöscht, wird bei demselben Speicherbereich der Wert verringert. Das ist nicht threadsafe, aber sollte funktionieren (ich habe es nicht ausprobiert).
Sieht etwa so aus:
C++:
dynVar::dynVar(dynVar& c) {
//This does NOT create a deep copy (yet)
this->m_reference_count = c.m_reference_count;

(*m_reference_count)++;
this->data = c.data;
this->type = c.type;

}

dynVar::dynVar(dynVar&& c) {
//Set the reference count
this->m_reference_count = c.m_reference_count;
//Do NOT increase reference count, but remove data from c
if (*c.m_reference_count == 0) c.m_reference_count = NULL; //Frag nicht, was das "if" hier tut - ich weiss es gerade auch nicht, bin aber zu müde, Entscheidungen zu fällen :)
this->data = c.data;
c.data = NULL;
this->type = c.type;

}

sizeof liefert ein size_t, casten zu int kann theoretisch andere Ergebnisse bringen
(bei so großen Werten, wie man vermutlich nie von sizeof bekommen wird...)
Das ist mir bewusst. Der Cast ist auch nur drin, weil ich zu faul war, die printf()-Formatierung für size_t rauszusuchen. Dürfte irgendwas in Richtung %lu oder so sein, oder?
Aber das Ergebnis würde wohl dasselbe sein ;)

Virtuelle Funktionen führen dazu, dass es in den Objekten sog. Vtable-Pointer gibt; Pointer,
die zu den für sie gültigen Funktionen zeigen. Braucht eben eine Pointergöße (4,8,...).

Bzw. man kann (als C++-Compilerhersteller etc.) das Problem, die passende Funktion
zu ermitteln, auch auf andere Arten lösen, aber diese Vtablepointersache ist das
Übliche auf allen mir bekannten Systemen.
Das dachte ich mir. Ursprünglich hatte ich sogar die Befürchtung, dass jede virtuelle Funktion gleich ein Function Pointer ist. Aber das wäre wohl viiiiiiel zu ineffizient...
Was allerdings seltsam ist, ist, dass ein nicht-virtueller override dann gleich auch zu virtual wird. Das ginge doch leichter, also als "normale" Funktion? Oder kann der Compiler das nicht korrekt auflösen?

Nicht ganz sicher, was du damit meinst. Wenn du einen Konstruktor hast, der
Parameter nimmt, und den zum Objekterstellen verwendest, ist auch alles ok.
Einfach jeder Konstruktor, der von C++ erlaubt ist, passt. Was bei Objekterstellung
außer dem evt. eigenen Konstruktorinhalt gemacht wird ist eben die Befüllung der
Vtablepointer, das generiert der Compiler dazu.
Genau das meinte ich :)
Daher auch das/der/die Disassembly: Test2::Test2() ist mit keinem Code verbunden und macht eigentlich nix, alle anderen Konstruktoren werden erstellt und ausgeführt (eben wahrscheinlich das Setzen der vtables).

Nicht für die Vtablesache, ja. Und wenn man keinen eigenen Konstruktor
geschrieben hat (=der Konstruktor "leer" ist) braucht man auch dafür nichts.
Siehe oben.

Aber: Darauf kann man sich eig. nicht verlassen So ein Objekt mit malloc anzulegen und
dann zu verwenden, als wäre es "richtig" angelegt worden, ist laut Standard "undefined
behaviour", = alles mögliche kann passieren.
Ahja, da hatte ich auch mal Spass mit...
Irgendwie so machte ich das damals:
Code:
mov ecx, instance
call func
Wobei instance ein void* ptr war, der mit malloc() alloziert worden war. Der Versuch hatte ich mal gemacht, um Klassen ohne Header dynamisch aus DLLs zu laden. :) Und ja, das läuft nur auf Windows, *NIX benutzt für __thiscall das eax-Register (ja, nur auf 32 bit...).

Ahja. Nostalgie :)

new macht ja auch nicht viel anderes, als malloc() und dann den Konstruktor auf den Speicherbereich zu wirken, oder? (Mal von dem Exception-Zeug abgesehen).

Die Größe des Gesamtobjekts ist nicht unbedingt die Summe der Inhalte, kann auch größer sein,
abhängig davon, welche Variablenarten in welcher Reihenfolge in der Klasse sind (Alignment/Padding,
bei Bedarf genaueres dazu), und auch abhängig von Compilereinstellungen.

Ich hatte ehrlich gesagt immer gute Erfahrungen mit dem Padding und Alignment gehabt. Im schlimmsten Fall gibt es sonst ja noch #pragma pack :)

...ist leider nicht dafür bekannt, als Referenz geeignet zu sein. C99 (von 1999)
ist noch immer nicht vollständig implementiert, (auch in der neuesten Version
von VS nicht), und neue Sporachstandards sowieso nicht.
Jaja, soweit ich weiss, fehlte bis 2013 sogar C++11.
Aber Visual Studio als Komplettpaket (IDE, Tools und Compiler) ist halt schon etwas vom Besten, was man kriegt - und der Umfang hat sich mit 2015 und Community nochmals deutlich vergrössert. Ich mag VS für die Entwicklung lieber, muss aber sagen, dass msbuild als CLI absoluter Bockmist ist. Da ist gcc und g++ meilenweit voraus.
Von der Befolgung des Standards her: Persönlich habe ich selten etwas bemerkt (bis auf Fehlendes...).
Aufgrund der Verbreitung ist es dennoch eine Art Referenz: Auf Windows ist es halt der Standard.

Die Größe von string ist implementierungsabhängig.
Auch hier: Unter raw-Pointer-Grösse wird es nicht fallen und wie anfangs schon bemerkt: Das ist die Zahl, die mir VS liefert.

a) Die Poitnerlösung braucht nicht nur den Pointer und die Daten, sondern auch
Metadaten die beim OS fpr jede Allokierung anfallen. Dürften auch mehrere pointergröße Sachen sein.
Ich rate mal ins Blaue und würde sagen, dass std::string auch selbst wieder einen oder mehrere Pointer verwaltet. Das Ergebnis dürfte damit ziemlich gleich sein.
Die Metadaten: Nun, sicher mal die Grösse. Was braucht es sonst noch? (früher gab es ja noch free() mit Grössenangabe)

b) Manche Stringimplementierungen sind so gemacht, dass kurze Strings bis zu einer bestimmten
Maximallänge ohne Heapallokierung direkt im Objekt gespeichert werden können (nur der
sizeof-Speicherverbrauch, und schnellerer Zugriff)
Uh, bringe mich nicht in Versuchung, den Pointer auf (char[4]) bzw. (char[8]) zu casten :p Irgendwo hat man immer ein Bit für eine Flag übrig :)

Anderes Thema: Hast du bei einer Scriptsprache an Charsets gedacht?
Codedatei allgemein bzw. Stringliterale...
Sehr. Alles wird als UTF-8 gelesen und dann in wchar_t's gepackt. Und dann alles auf chars runtergebrochen :p
Aber dadurch, dass es rohe pointer sind, sollte es möglich sein, Unicode zu verarbeiten - ich habe im Moment einfach nicht wirklich Lust darauf...

Wie gesagt, Objektorientierung mit allem drum und dran macht das Programmieren oft einfacher,
gerade für größere Projekte, als prozedurales C. Die Vorteile überwiegen die zusätzlichen Pointer
im Speicher bei Weitem.
Irgendwie arbeite ich lieber mit Rawpointern als mit reinterpret_cast. Und Vorteile gibt es ja eigentlich (fast) nur für den Programmierer, oder? Ich weiss nicht wieso, aber ich habe immer Angst davor, dem Compiler zu viel zu überlassen.

b) Man braucht für jede einfache int.-Variable im Script relativ viel Speicher
(deine Objekte, Heapmanagement, Name)
Aufgrund der Idee von oben werde ich mich wohl in den nächsten Tagen ransetzen und die Pointer auf int casten. Hui, wird das ein Spass :)
Übrigens speichert keine Variable ihren eigenen Namen. Die Namen werden von der Engine auf Variablen gemappt. Daher reichen auch 12 Bytes (Ich würde lieber 8 Bytes haben, damit man die Dinger direkt in ein Register kriegt, aber so muss immerhin nicht viel kopiert werden :p).

Etwas union-basiertes wäre eine andere Möglichkeit, deutlich weniger
Speicherbelastung wegen Heapvermeidung für einfache Variablen:
Das hatte ich gar nicht auf dem Schirm. Danke, werde ich mir anschauen :)

Wenn deine Scriptsprache sowas wie lokale Variablen mit automatischer
Lebensdauer und geschachteltem Scope hat, also (in C++-Art) sowas:
Ich hatte in einem ersten Schritt an passive Überschreibung gedacht: Die Variabeln bleiben solange im Speicher, bis die main() endet oder eine andere Variable gleichen Namens überschreibt.
Scopes gibt es zwar, sie machen aber (noch) nichts. Ich muss erst noch die Klassen und Memberfunction-Aufrufe korrekt hinbiegen, dann den mühsamen Code schön wrappen (hm. Code "rappen" wäre auch mal eine Idee... :D ) und dann muss das alles noch korrekt laufen.
Dann muss ich noch optimieren, da es sehr schnell laufen sollte, und dann plane ich noch ein natives Interface (also so, dass man im Script native Bereiche angeben und das Skript dann durch den C-Compiler jagen kann und am Ende ein Script hat, in dem einige Funktionen interpretiert und andere direkt ausgeführt werden. (Nein, nicht für Viren, sondern für Computerspiele :)) Der Hintergrund ist, dass ich Unitys GC-Konzept gesehen habe. Nuff said. ^^

Gruss
cwriter
 
Der C++-Standard verlangt, dass jedes Objekt seine eigene Adresse hat oder so ähnlich.

Huh?

Der Grund warum nicht 0 zurückgegeben wird ist:
§5.3.3 Abs. 2 hat gesagt.:
The size of a most derived class shall be greater than zero (1.8)

/EDIT:
Abgeleitet aus:
§1.8 Abs. 5 hat gesagt.:
Unless it is a bit-field (9.6), a most derived object shall have a non-zero size and shall occupy one or more bytes of storage. Base class subobjects may have zero size. An object of POD5) type (3.9) shall occupy contiguous bytes of storage.
 
Zuletzt bearbeitet:
@Cromon ...und aus der Erklärung, was eine Adresse ist, im nächsten Absatz( §1.8 Abs. 6),
folgt dann, dass jedes der betroffenen Objekte seine eigene Adresse hat.

Ja, hab die Richtung der Folgerung verdreht, war nur aus dem Gedächtnis.
 
Was allerdings seltsam ist, ist, dass ein nicht-virtueller override dann gleich auch zu virtual wird. Das ginge doch leichter, also als "normale" Funktion? Oder kann der Compiler das nicht korrekt auflösen?
Virtual "vererbt" sich auch. Also auch wenn man in der Kindklasse zu einer Funktion nicht virtual
dazuschreibt, ist sie virtual, wenn die entsprechende Funktion in der Vaterklasse virtual war.
(std. 10.3.2)


new macht ja auch nicht viel anderes, als malloc() und dann den Konstruktor auf den Speicherbereich zu wirken, oder? (Mal von dem Exception-Zeug abgesehen).
Vom Prinzip her ja. Malloc (oder etwas vergleichbares), und dann die Konstruktion
(die eben nicht nur aus der Konstruktorfunktion besteht)

Alles wird als UTF-8 gelesen und dann in wchar_t's gepackt. Und dann alles auf chars runtergebrochen :p
Ich muss dich leider davon informieren, dass wchar_t für UTF8 eher nicht sinnvoll ist :p

...wenn man erst später beachtet, dass nicht jedes Byte ein Buchstabe ist usw.
kann da einiges zum umschreiben werden

Irgendwie arbeite ich lieber mit Rawpointern als mit reinterpret_cast.
Die schließen sich gegenseitig nicht aus.

Und Vorteile [von OO] gibt es ja eigentlich (fast) nur für den Programmierer, oder?
Ja, schon.

Was du noch alles vorhast [letzte Absätze] klingt nach einigem Aufwand :)
 
Virtual "vererbt" sich auch. Also auch wenn man in der Kindklasse zu einer Funktion nicht virtual
dazuschreibt, ist sie virtual, wenn die entsprechende Funktion in der Vaterklasse virtual war.
(std. 10.3.2)
Um direkt mit Links zu arbeiten, hier mal der Link zum Working Draft.
Allerdings steht da nicht, warum das so sein muss. Oder ein bisschen anders ausgedrückt: Wenn ein Programmierer "int not_changeing = 123;" schreibt und die Variable als Konstante benutzt, nimmt sich der Compiler ja auch die (durch den Standard gegebene) Freiheit, die Variable durch eine Konstante zu ersetzen. Warum tut er es nicht, wenn eine Klasse eigentlich nur eine virtuelle Klasse überschreibt und kein (mir ersichtlicher) Grund besteht, es virtual zu lassen?

Ich muss dich leider davon informieren, dass wchar_t für UTF8 eher nicht sinnvoll ist :p

...wenn man erst später beachtet, dass nicht jedes Byte ein Buchstabe ist usw.
kann da einiges zum umschreiben werden
Das ist mir bewusst. Allerdings bietet wchar_t (unter Windows 2 Bytes gross) schon eine grosse Abdeckung und bleibt dabei effizienter, als wenn man jedes mal das Encoding behandeln müsste. Daher wird gleich nach dem Laden das UTF-8 zu wchar_t konvertiert.
(Hier ist der verwendete Weg: http://stackoverflow.com/a/18374698)

Die schließen sich gegenseitig nicht aus.
Stimmt. Ich wollte wohl sagen, dass ich lieber "caste" als "typesafen" (rip in peace Englisch und Deutsch...) Code zu schreiben. Irgendwie nimmt die Typesafety mir den Spass am Programmieren. Ist wie im Kino: Spass hat man, wenn es rummst ;) (Das Debuggen ist dann eine andere Sache... Aber es gilt: No risk, no fun :p)

Was du noch alles vorhast [letzte Absätze] klingt nach einigem Aufwand :)
Ja, aber um ehrlich zu sein: Ich bezweifle, dass etwas Brauchbares daraus wird. Aber gerade zum Lernen sind solche Game-Engine-Spielwiesen perfekt geeignet: Man kann sehr viele verschiedene Bereiche behandeln und findet so immer wieder eine Unklarheit, zu deren Klärung man das Forum seines Vertrauens hinzuziehen kann. Ich finde Tutorials und Bücher da etwas sehr trocken, da man die Probleme selbst nicht sieht. Aber die Lehrmittel aus dem renommierten Autodidakt-Verlag sind ja (fast) immer die besten :)

Gruss
cwriter

/EDIT: So langsam hat sich Cromon die Plakette "Mr. Standard" verdient, oder? :p
 
Wenn ein Programmierer "int not_changeing = 123;" schreibt und die Variable als Konstante benutzt, nimmt sich der Compiler ja auch die (durch den Standard gegebene) Freiheit, die Variable durch eine Konstante zu ersetzen.
Die Variable betreffend:
Ja, wenn beim Verwendungszeitpunkt garantiert werden kann, dass der Wert noch unverändert ist
(wobei es für das Garantieren ein paar per Std. erlaubte Grundannahmen gibt
(direkt beschrieben und abgeleitet :))).

Virtual wird vererbt, weil es so festgelegt wurde, auch wenns vllt. keinen Sinn macht.

Und warum der Virtualpointer immer nötig ist:
a) Garantieren dass für Name X von Klasse Y nur die Funktion Z gemeint sein kann ist noch komplizierter
b) Wenn man bedenkt, dass der Compiler einzeln auf verschiedenen cpp-Files arbeitet, die sich gegenseitig
brauchen [können] ... einen Teil soweit optimieren, dass es für den nächsten Teil nicht mehr verwendbar ist,
geht natürlich nicht.
c) ...

Bei einzelnen Methodenaufrufen das Nachschauen in der Vtable wegzuoptimieren geht,
wenn nur eine Implementierung in Frage kommt. Aber eben nur, wenn es der Compiler erkennt,
was oft nicht der Fall ist.

Das ist mir bewusst. Allerdings bietet wchar_t (unter Windows 2 Bytes gross) schon eine grosse Abdeckung und bleibt dabei effizienter, als wenn man jedes mal das Encoding behandeln müsste. Daher wird gleich nach dem Laden das UTF-8 zu wchar_t konvertiert.
Sorry, aber irgendwie macht das keinen Sinn. wchar_t ist ein (auf Windows 2 Byte großer) int-typ
und hat so allein überhaupt nichts mit Encoding zu tun bzw. kann jedes Encoding der Welt speichern.

Ich vermute, du meinst UTF16 (LE, ohne BOM), wie es in der Winapi häufig verwendet wird.
Während UTF8 1 bis 4 Byte pro Glyph (nicht Character) braucht, hat UTF16 2 oder 4.
Nicht einfach nur 2. Damit bieten sich einzelne bzw. Paare von wchar_t zwar zur Abspeicherung
an, aber man hat nach wie vor nicht den Luxus, jeden Arrayindex als ein Zeichen zu verarbeiten
(usw.usw).

Zwei weitere Punkte:
*Wenn die Quelldatei nicht UTF8 ist (sondern ISO88591 oder irgendwas) geht das Ganze daneben.

* "effizienter, als wenn man jedes mal das Encoding behandeln müsste"
Mit der Konvertierung hast du die Behandlung doch schon angefangen :p
Ja, ich weiß, dass ich lästig bin :D
 
Bei einzelnen Methodenaufrufen das Nachschauen in der Vtable wegzuoptimieren geht,
wenn nur eine Implementierung in Frage kommt. Aber eben nur, wenn es der Compiler erkennt,
was oft nicht der Fall ist.
Der Compiler müsste es ja nicht machen. Aber es erscheint mir komisch, dass der Standard das Optimieren hier explizit verbietet.

Sorry, aber irgendwie macht das keinen Sinn. wchar_t ist ein (auf Windows 2 Byte großer) int-typ
und hat so allein überhaupt nichts mit Encoding zu tun bzw. kann jedes Encoding der Welt speichern.
Jep.
Ich vermute, du meinst UTF16 (LE, ohne BOM), wie es in der Winapi häufig verwendet wird.
Während UTF8 1 bis 4 Byte pro Glyph (nicht Character) braucht, hat UTF16 2 oder 4.
Nicht einfach nur 2. Damit bieten sich einzelne bzw. Paare von wchar_t zwar zur Abspeicherung
an, aber man hat nach wie vor nicht den Luxus, jeden Arrayindex als ein Zeichen zu verarbeiten
(usw.usw).
Nope. :)
Ich lade das Skript als UTF-8 BOM von der Platte und wandle es da von UTF-8 zu int um (also eigentlich wchar_t also short). Es gibt dann kein Encoding mehr, sodass das Codieren nur beim Laden anfällt. Der Code:
C++:
int basicLoadFile(const std::wstring& filepath, std::wstring& out)

{
    //File is generally UTF-8
    FILE* f;
    _wfopen_s(&f, filepath.c_str(), L"r");
    if (f == NULL)
        return -1;

    fseek(f, 0, SEEK_END);
    size_t filesize = ftell(f);

    rewind(f);

    char* filedata = (char*)calloc(filesize + 1, 1);
    if (filedata == NULL) {
        perror("ALLOCATION ERROR");
        return -2;

    }
    fread(filedata, 1, filesize, f);
    fclose(f);
    //Convert
    std::wstring_convert<std::codecvt_utf8<wchar_t>> convert;
    out = convert.from_bytes(filedata);

    free(filedata);
    return 0;

}
(Kann sein, dass es Bugs hat; ASCII funktioniert zumindest.

*Wenn die Quelldatei nicht UTF8 ist (sondern ISO88591 oder irgendwas) geht das Ganze daneben.
Ja; ausser, der Bereich des ersten Bytes reicht.
* "effizienter, als wenn man jedes mal das Encoding behandeln müsste"
Mit der Konvertierung hast du die Behandlung doch schon angefangen :p
Wie schon gesagt: Konvertieren muss man immer mal. Aber nur einmal und nicht jedes Mal.

Ja, ich weiß, dass ich lästig bin :D
Nicht doch :)
Fehler in der Diskussion zu finden sind immer besser als dann die Datenleiche aus den Bytes ausgraben zu können :p

Gruss
cwriter
 
Um direkt mit Links zu arbeiten, hier mal der Link zum Working Draft.
Allerdings steht da nicht, warum das so sein muss. Oder ein bisschen anders ausgedrückt: Wenn ein Programmierer "int not_changeing = 123;" schreibt und die Variable als Konstante benutzt, nimmt sich der Compiler ja auch die (durch den Standard gegebene) Freiheit, die Variable durch eine Konstante zu ersetzen. Warum tut er es nicht, wenn eine Klasse eigentlich nur eine virtuelle Klasse überschreibt und kein (mir ersichtlicher) Grund besteht, es virtual zu lassen?

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:
If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, [...] then Derived::vf is also virtual (whether or not it is so declared) and it overrides112 Base::vf.

Wenn die Implementation eine VMT verwendet, dann hättest du ja auch nichts davon wenn deine Funktion nicht mehr virtuell wäre in der Kindklasse. Aber das ist natürlich gar nicht möglich, in der Kindklasse kannst du die Funktionen in der VMT nicht plötzlich anders anordnen oder gar gewisse Funktionen auslassen, sonst hätte jemand, der mit der Basisklasse arbeitet ja das Problem, dass er gar nicht weiss, was für eine Funktion da aufgerufen wird.
 
Zurück