Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

19.04.2011, 11:59

Fragen zur Objektorientiertheit der TriBase-Engine

Hallo zusammen!

Immer wieder kamen mir beim Lesen des Quellcodes der TriBase-Engine ein paar Fragen zur Objektorientiertheit der Engine auf. Es soll keinesfalls besserwisserisch klingen, aber teilweise habe ich das Gefühl, die Objektorientiertheit kann verbessert werden. Andererseits kann meine Herangehensweise auch völlig falsch sein (bzgl. Performance oder Fehldenken).
Hier mal ein Beispiel anhand von tbPlane:

- Wie bei anderen Klassen auch, gibt es wieder viele globale Funktionen wie tbPlaneNormalize. Warum werden diese nicht statische Funktionen der Klasse "Plane" und haben dann einfache Namen wie "Normalize()" in diesem Fall?
- Ähnliches könnte man auch nicht-statisch mit tbPlaneDotNormal machen und sich ggf. den Parameter p sparen, dem eine Plane übergeben wird. Es wird dann einfach die Plane genommen, auf die die Funktion angewendet wird.
- Die Funktionen tbPlaneFromPointNormal und tbPlaneTransform könnten doch auch gut Konstruktoren sein? Die Parameterliste würde sich dann immer noch eindeutig von anderen Konstruktoren unterscheiden.
- Vermutlich Unwissen von mir als ursprünglicher C#-Programmierer: Warum werden so viele Dinge im Header gleich in der Konstruktorliste erledigt? Bringt dies einen Performancevorteil? Ich würde es rein optisch schöner finden, die Konstruktoren von z.B. tbVector3 im Header zwar zu nennen, deren Funktionalität aber in der CPP-Datei anlegen (z.B.

C-/C++-Quelltext

1
Vector3(const float, const float, const float);

in Kombination mit

C-/C++-Quelltext

1
2
3
4
5
6
Vector3::Vector3(const float x, const float y, const float z)
{
    this->x = x;
    this->y = y;
    this->z = z;
}


Würde mich um jede Meinung bzw. Richtigstellung meiner Vermutungen freuen :)

Gruß,
Pac-Man

2

20.04.2011, 08:43

Ich hab das aktuelle Buch nicht, aber zumindest von der ersten Auflage weiß ich, dass einige Probleme recht hässlich gelöst wurden :D

Die Initialisierungsliste vom Konstruktor zu verwenden ist theoretisch schneller, aber ich kann es mir gut vorstellen, dass ein guter Compiler Zuweisungen im Konstruktor wegoptimieren kann. Auf jeden Fall kann man aber beides in der cpp Datei verwenden, wo der Code steht kann höchstens die Compilierzeit beeinträchtigen, nicht das Ergebnis.
Die anderen Punkte hören sich auch so an, als könne man sie gut so umsetzen wie du meinst. Normalize sollte in der Ebenenklasse dann aber natürlich nicht-statisch sein, damit es das aktuelle Objekt ändert, so erspart man sich evtl. sogar noch eine Kopie.

Achja: Natürlich ist const immer schön, aber da in deinem Fall die floats eh kopiert (call by value) werden, bringt es meiner Meinung nach an dieser Stelle nicht viel, man kann halt die Parameter in der Funktion nicht ändern, bzw. müsste sie dafür kopieren. Ich würd es hier einfach weglassen, der Codekompaktheit wegen.
Lieber dumm fragen, als dumm bleiben!

3

20.04.2011, 09:31

Hallo Jonathan! Danke für die Antwort. Ich habe dazu noch eine Frage bzw. Anmerkung:

Normalize sollte in der Ebenenklasse dann aber natürlich nicht-statisch sein, damit es das aktuelle Objekt ändert, so erspart man sich evtl. sogar noch eine Kopie.

Das stimmt. Bisher dachte ich immer so, dass ich die Ebene, von der Normalize angewendet wird, unverändert bleibt und eine neue Ebene, die dann normalisiert ist, zurückgegeben wird - wie bei einem String in .NET, da sind Strings sozusagen auch "unveränderlich" und das Ausführen von String.TrimRight() liefert einen neuen String ohne Leerzeichen am Ende zurück, der eigentliche String bleibt wie er ist).
Aber du hast wohl Recht und die Kopien sind einfach zu langsam.

Natürlich ist const immer schön, aber da in deinem Fall die floats eh kopiert (call by value) werden, bringt es meiner Meinung nach an dieser Stelle nicht viel, man kann halt die Parameter in der Funktion nicht ändern, bzw. müsste sie dafür kopieren. Ich würd es hier einfach weglassen, der Codekompaktheit wegen.

Das ist erstaunlich, da es soweit ich weiß ja überall in der TriBase-Engine gemacht wird :D

Gruß,
PacMani

Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

4

20.04.2011, 12:04

Das Const bei den Parametern ist aber nicht schlecht. Dazu solltest du dir mal Effective C++ angucken. Ein wirklich gutes Buch. Dort wird das auch nochmal mit der Parameterliste des Konstruktors beschrieben. Es geht dabei grob darum, wann Variablen und Objekte initialisiert werden und wann sie sich in welchem Zustand befinden. Const und die Initialisierungsliste sind beide nicht (unbedingt) fehl am Platz. Und ja die globalen Funktionen sollten statisch in der Klasse umgesetzt werden. Ob man Normalize nun statisch macht oder nicht weiß ich nicht. Ich habe schon mehrfach gesehen, dass dies static ist. Ob das dann natürlich der schönste Weg ist ist fraglich;) Aber mir hat es bis jetzt so immer ganz gut gefallen.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

5

20.04.2011, 16:13

OK, jetzt habe ich bei der Sache mit den consts zwei Meinungen. Ich würde jetzt const drinlassen. Wobei der direkte Vorteil noch nicht genannt wurde :O
Klar ist, dass ich mit meiner "Klassifizierung" der vielen globalen Funktionen schon richtig angefangen habe.

Nur ob Methoden jetzt statisch sind, und eine veränderte Kopie zurückgegeben werden soll oder die Instanz, auf die die Methode ausgeführt wird, verändert werden soll, das bedürfte wohl einer Umfrage. Ich könnte mich mit beidem anfreunden. Die Frage ist nur, was der User später denkt, der ein simples Include in seinen Header packt und dann darauf losprogrammieren möchte. :lol:

6

20.04.2011, 17:00

Naja, richtig Sinn macht const halt bei sowas:

C-/C++-Quelltext

1
2
3
4
void Function(const Obj& MyBigClass)
{
 Obj.doSOmething();
}


Die Übergabe per Referenz ist halt schneller, aber da man dann das Objekt ändern könnte, was zu unerwünschten Seiteneffekten führen könnte, macht man die Referenz halt Konstant.
Du kannst auch eine Konstante Referenz auf ein float übergeben, nur das macht keinen Sinn, es es egal ist, ob man die 4 Byte für ein float oder die 4 Byte für die Referenz (die ja praktisch wie ein Zeiger ist) kopieren muss (natürlich können die Werte je nach Plattform abweichen, blabla). Deswegen übergibt man diese Primitiven Typen meist nicht per Referenz.
In deinem Fall geht durch das const einfach das hier nicht:

C-/C++-Quelltext

1
2
3
4
5
6
7
Vector3::Vector3(const float x, const float y, const float z)
{
    x=7; //Fehler, x ist const
    this->x = x;
    this->y = y;
    this->z = z;
}

Wieso man das verbieten sollte, weiß ich nicht, da wie gesagt call-by-value benutzt wird, würden keine unerwünschten Seiteneffekte auftreten.

Zitat

Aber du hast wohl Recht und die Kopien sind einfach zu langsam.

Ach, das würde man eher rein aus Prinzip verbieten, einen wirklichen Unterschied wird man fast nie merken. Man sollte ja eh immer erst die Algorithmen optimeiren, bevor man deren Implementierung optimiert.


Effektiv C++ C++ Lektion 19 gibt ein paar Tipps, wann Funktionen global und wann Elementfunktionen sein sollten. Grobe Zusammenfassung:
- Elementfunktionen können virtuell sein, globale nicht
- operator << und operator >> sind generell globale FUnktionen
- Nur globale Funktionen erlauben implizite Typumwandlung des linken Arguments
- sonst soltle man in der Regel Elementfunktionen benutzen.
Lieber dumm fragen, als dumm bleiben!

7

20.04.2011, 18:30

Ok, klingt sinnvoll. Dann entscheide ich mich für
- Const bei Werttypen weg
- Elementfunktionen sowieso
- Keine Kopien, sondern bestehende Instanz innerhalb der Methode verändern

Wo du gerade von Operatoren sprichst:
Kann es sein, dass die Operatoren +, -, * etc. immer in der Headerdatei deklariert werden müssen oder ähnlich? Vielleicht habe ich auch etwas anderes falsch gemacht, aber VS zickte durchgehend rum bis ich den Code in die Headerdatei verschob.
Kann es momentan leider nicht reproduzieren um eine genaue Fehlermeldung zu zeigen. Deswegen frage ich, ob es bei Operatoren irgendwo diese Muss-Regelung mit dem Code in der Headerdatei gibt.

Jetzt habe ich die Normalize()-Funktion so umgesetzt:

C-/C++-Quelltext

1
2
3
4
void Vector3::Normalize()
{
    *this /= sqrtf(x * x + y * y + z * z);
}

Sollte man den veränderten Vektor dennoch zurückliefern? Hin und wieder würde man ja nur sagen:

C-/C++-Quelltext

1
2
Vector3 v(20, 30, 50);
v.Normalize();

und eigentlich gehört es ja nicht zum guten Ton, den Rückgabewert einfach zu ignorieren, oder?

Würde dann so aussehen, damit ja nicht wieder eine Kopie zurückgegeben wird:

C-/C++-Quelltext

1
2
3
4
Vector3& Vector3::Normalize()
{
    return *this /= sqrtf(x * x + y * y + z * z);
}

...und angewandt zum Beispiel so:

C-/C++-Quelltext

1
return Vector3(a + s * (b - a).Normalize());



Bei einigen Funktionen müsste ich allerdings const bei der Parameterliste auch bei Matrizen und Vektoren rausnehmen, da ich sonst z.B. von einem Vektor die Länge nicht mehr abfragen kann - die Length-Methode ist ja nicht static:

C-/C++-Quelltext

1
2
3
4
5
6
    Vector2 Matrix::TransformNormal(const Vector2& v, const Matrix& m)
    {
        // Vektorlänge berechnen
        const float length = v.Length(); // <--- Fehler
        [...]
    }

Was sollte ich nun machen? Ich dachte an folgendes:
- const in der Parameterliste entfernen. Unschön, da uneinheitlich bei den ganzen Matrix-Methoden
- static bei der Length-Methode. Was passiert dann bei Vertizen, die noch garnicht initialisiert sind? Auch irgendwie eine halbe Lösung.
- Ein besserer Lösungsvorschlag, der mir nicht einfällt :/

Gruß,
Pac-Man

Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von »Pac-Man« (21.04.2011, 10:14)


8

21.04.2011, 14:26

DU kannst bei konstanten Objekten nur Methoden aufrufen die selbst als const deklariert sind, diese können dann kein Attribute des Objektes mehr verändern. Man würde erwarten, dass v.Length() nur die Länge des Vektors zurückgibt und ihn nicht verändert, also sollte Length const deklariert sein.
http://www.parashift.com/c++-faq-lite/const-correctness.html (insbesondere 18.10)

Operatoren sollten sich eigentlich wie normale Funktionen verhalten, sie müssen in der Header DAtei deklariert werden können aber an beliebiger Stelle definiert werden.

Ob man das Ergebnis zurück gibt ist wohl Geschmackssache. Allerdings solltest du aufpassen, was der User vermutet. Wenn er mit v.Normalize() das normalisierte Ergebnis zurückbekommt, könnte er denken, v selbst würde dadurch nicht verändert und unerwartete Seiteneffekte haben. Ich glaube ich fände es am schönsten, wenn .Normalize() einfach normalisieren würde, und eine zweite Funktion .Normalized() nur das Ergebnis zurück gibt, das Objekt aber selbst nicht verändert (und daher natürlich auch als const deklariert ist).
Lieber dumm fragen, als dumm bleiben!

9

21.04.2011, 14:34

Ja, da ist was dran bezüglich Usererwartung. Ich habe anfangs auch erst gedacht, dass das eigentliche Objekt nicht verändert wird. Jetzt mache ich es aber doch anders.

Zum Ziel habe ich mir noch etwas anderes; entweder Code drumherumwrappen oder die Engine nach außen hin anders gestalten, so dass User sogenannte Entitäten erstellen und die dann drehen, rotieren etc. ohne sich direkt um Matrizen und Vektoren kümmern zu müssen (ähnlich wie ein OO-Blitz3D). Die Normalize()-Funktion nutze ich dann nur noch intern.

Danke für die Tipps!

EDIT: Den Fehler mit den arithmetischen Operatoren habe ich gefunden. Sie müssen ja global definiert sein:

C-/C++-Quelltext

1
inline Vector3 operator +(const Vector3& a, const Vector3& b);


Und in der Codedatei stand folgendes:

C-/C++-Quelltext

1
2
3
4
Vector3 Vector3::operator +(const Vector3& a, const Vector3& b)
{
    return Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}


Natürlich habe ich den Operator nicht in der Klasse sondern global definiert, weswegen die "Namespace"-Angabe in der Codedatei falsch war.

Was ist eigentlich, wenn man sie lokal definiert? Unter http://de.wikibooks.org/wiki/C%2B%2B-Pro…%C3%9Cberladung steht:

Zitat

Wird der Operator als globale Funktion implementiert, müssen beide Operanden als Parameter angegeben werden.

Bruch operator + (Bruch b1, Bruch b2);

Dies ist notwendig, wenn der Typ des ersten Operanden ungleich der jeweiligen Klasse sein soll:

einBruch + 1 // Implementierung als Member-Funktion möglich
1 + einBruch // Implementierung als Member-Funktion nicht möglich

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »Pac-Man« (21.04.2011, 17:30)


10

21.04.2011, 20:42

Was genau ist deine Frage bei den Operatoren?
Sie können Member oder Global sein. Nur wenn sie global sind, kann auf der linken Seite was anderes wie die Klasse stehen.

Als member siehts dann so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
class Vector
{
 Vector operator + (Vector v2)
 {
   return Vector(x+v2.x, y+v2.y);
 }
 float x, y;
}
Lieber dumm fragen, als dumm bleiben!

Werbeanzeige