Funktion für mehrere Klassen

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Funktion für mehrere Klassen

Beitrag von starcow »

Ich bin gerade dabei die Struktur meines Kollisions-Programmes umzugestalten und muss feststellen, dass ich da wohl etwas falsch verstanden habe. :-/
Aber vielleicht ist es mit eurer Hilfe noch zu "retten".

Wenn ich (als Beispiel) eine Basisklasse habe "Tier" und eine davon abgeleitete Klasse "Ente", ist es dann irgendwie möglich eine Funktion zu schreiben "Futtern", die mir alle abgeleiteten Klassen von "Tier" füttert?
Die Funktion würde beim füttern auch nur auf Attribute zurück greifen, die von der Basisklasse "Tier" bereitgestellt wurden.

Die Funktion soll halt als Parameter sowohl eine reine Tier-Instanz (Basisklasse) entgegen nehmen können, als auch eine davon abgeleitete Klasse, also z.B. eine Enten-Instanz.
Eigentlich hatte ich erwartet, das das geht, da eine Ente ja ein Tier ist.
Ich könnte jetzt natürlich die Parameterliste überladen, was ich aber unbedingt vermeiden will, da es dann wirklich zweimal der selbe Code wäre.

Code: Alles auswählen

futtern(CLS_Tier& tier) {
tier.energie++;
}

CLS_Ente ente;

futtern(ente); // Fehler, da nicht zufrieden mit dem Daten-Typ
Da das ganze zeitkritisch ist, dürfte für die Lösung nicht extra Resourcen verbraten werden.

Gibts da ne Lösung, oder muss ich diesen Ansatz vergessen?

Gruss starcow
Freelancer 3D- und 2D-Grafik
mischaschaub.com
NeuroCoder
Beiträge: 14
Registriert: 24.05.2003, 17:31
Alter Benutzername: NeuroCoder
Wohnort: NRW
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von NeuroCoder »

Hi starcow,

das kompiliert bei mir:

Code: Alles auswählen

#include <iostream>

class Tier
{
public:
	int energie;
};

class Ente : public Tier { };

void futtern(Tier& t)
{
	++t.energie;
}

int main(int argc, char** argv)
{
	Ente e;
	futtern(e);
	std::cout << e.energie << std::endl;
	return 0;
}
Was ist denn der Compiler-Fehler? Hast Du das "public" bei der Basisklasse von Ente vergessen?

Viele Grüße
NeuroCoder
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Hmmm sehr seltsam...
Tatsächlich kann ich aus anderen Gründen noch gar nicht compilieren, weil da noch einige Baustellen offen sind.
Allerdings unterlegt mir VisualStudio die beiden Argumente im Funktionsaufruf mit rot.
Vielleicht liegts auch daran, dass es keine einzelne Instanzen, sondern Vektoren, gefüllt mit solchen Instanzen sind.
Visual Studio meint:
no suitable user-defined conversion from ... to ... exists
Gruss starcow

Edit:
Es liegt wohl an den Vektoren

Code: Alles auswählen

class Tier
{
public:
	int energie;
};

class Ente : public Tier { };

void futtern(std::vector<Tier>& tier_VEC) {
	++tier_VEC.at(1).energie;
}

int main(int argc, char** Argv) {
	std::vector<Ente> enten_VEC;
	Ente ente;
	enten_VEC.push_back(ente);

	futtern(enten_VEC);
	return 0;
}
error C2664: 'void futtern(std::vector<Tier,std::allocator<Tier>> &)': cannot convert argument 1 from 'std::vector<Ente,std::allocator<Ente>>' to 'std::vector<Tier,std::allocator<Tier>> &'
Das sollte doch eigentlich keinen Unterschied machen, ob ich ein einzelnes Element oder ein Vektor mit mehreren Elementen übergebe?
Ich dachte eigentlich, man übergibt ganze Vektoren immer als Referenz. Oder ist das dann unsicher, weil sich allenfalls die Speicheradresse ändern könnte?
Freelancer 3D- und 2D-Grafik
mischaschaub.com
NeuroCoder
Beiträge: 14
Registriert: 24.05.2003, 17:31
Alter Benutzername: NeuroCoder
Wohnort: NRW
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von NeuroCoder »

Achso, Du meinst sowas:

Code: Alles auswählen

class A { };
class B : public A { };
void f(const std::vector<A>& v)
{
    // ...
}

void g()
{
    std::vector<B> v;
    f(v); // <-- Fehler
}
Anders als in Java sind Arrays und auch STL Container in C++ nicht kovariant. Du musst also jedes Element einzeln übergeben:

Code: Alles auswählen

#include <algorithm>
#include <vector>

class A { };
class B : public A { };

void f(A& a) { /* snip */ }

void g()
{
	std::vector<B> d(10);
	std::for_each(d.begin(), d.end(), &f);
}
Viele Grüße
NeuroCoder
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Oh Schreck... Dank dir, NeuroCoder.
Gibts da vielleicht noch eine alternative Möglichkeit?
Da ich in der Funktion auf die Auswertung eingehe (ich muss Attribute aus der Basisklasse miteinander vergleichen), kann ich diese nicht verlassen, ehe ich alle Elemente durch iteriert habe.
Freelancer 3D- und 2D-Grafik
mischaschaub.com
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Funktion für mehrere Klassen

Beitrag von Alexander Kornrumpf »

NeuroCoder hat geschrieben: 18.09.2020, 19:44 Anders als in Java sind Arrays und auch STL Container in C++ nicht kovariant. Du musst also jedes Element einzeln übergeben:
In Java kannst du auch kein List<Bar> als List<Foo> übergeben. Du musst List<? extends Foo> als Typen verwenden. (Nicht mit einem Compiler geprüft aber ich bin mir doch recht sicher.)

Das C++ Äquivalent wäre meiner Meinung nach, die Funktion selbst generisch zu machen.

Siehe auch:
starcow hat geschrieben: 18.09.2020, 17:17 Ich könnte jetzt natürlich die Parameterliste überladen, was ich aber unbedingt vermeiden will, da es dann wirklich zweimal der selbe Code wäre.

Genau das Problem lösen ja templates.
NeuroCoder
Beiträge: 14
Registriert: 24.05.2003, 17:31
Alter Benutzername: NeuroCoder
Wohnort: NRW
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von NeuroCoder »

Alexander Kornrumpf hat geschrieben: 18.09.2020, 20:20 In Java kannst du auch kein List<Bar> als List<Foo> übergeben. Du musst List<? extends Foo> als Typen verwenden. (Nicht mit einem Compiler geprüft aber ich bin mir doch recht sicher.)
Stimmt. Mit Arrays geht es, mit List<T> nicht.
Alexander Kornrumpf hat geschrieben: 18.09.2020, 20:20 Das C++ Äquivalent wäre meiner Meinung nach, die Funktion selbst generisch zu machen.
Da stimme ich auch zu.
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Vielleicht etwas doofe Frage, doch was muss ich mir denn unter "generisch" Vorstellen?
Freelancer 3D- und 2D-Grafik
mischaschaub.com
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Funktion für mehrere Klassen

Beitrag von Alexander Kornrumpf »

starcow hat geschrieben: 20.09.2020, 12:11 Vielleicht etwas doofe Frage, doch was muss ich mir denn unter "generisch" Vorstellen?
Java hat eine Syntax die ein bisschen so aussieht wie C++ templates und nennt sie "Generics".

Der Wesentliche Unterschied ist meines Wissens, vereinfacht gesagt, dass der C++ Compiler wirklich eine Version des Templates für jeden genrischen Typ instanziiert wohingegen der Java Compiler das einfach nur als Annotation versteht, für die er Typechecks macht und ansonsten ignoriert.

Was du jedenfalls in C++ vermutlich machen willst, ist dass der Typ mit dem du tier_VEC parametrisierst, selbst ein Parameter von futtern ist.
NeuroCoder
Beiträge: 14
Registriert: 24.05.2003, 17:31
Alter Benutzername: NeuroCoder
Wohnort: NRW
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von NeuroCoder »

Genau. Konkret heißt das:

Code: Alles auswählen

class A
{
public:
    int x;
    void g() { }
};
class B : public A { };

template <typename T>
void f(std::vector<T>& v)
{
    for (auto& item : v)
        item.x = 2;
}

int main(int argc, char** argv)
{
    std::vector<B> v(10);
    f(v);
    
    std::vector<A> w(4);
    f(w);
    return 0;
}
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Jonathan »

Alternativ könntest du natürlich vector<Tier*> übergeben.

Falls das Problem noch nicht klar geworden ist: Im Vector liegen alle Objekte im Speicher hintereinander. Eine Ente ist aber unter Umständen größer als ein Tier, deshalb kann man ein vector<Ente> nicht als vector<Tier> interpretieren. Ggf. einfach mal das Memory-Layout aufmalen, dann sieht man es direkt.
Zeiger sind aber immer gleich groß, durch diese Indirektion tritt das Problem dann nicht mehr auf.
Mit vector<Tier*> können im Vektor auch unterschiedliche Tiere (Enten, Fische, etc.) drin sein. Mit der Template-Variante vector<T> müssen dann aber alle Objekte Enten sein.

Problem an der Pointer-Variante ist natürlich, dass du ggf. schon einen Vector<Ente> hast und dann einen zweiten Vector mit Zeigern erzeugen und füllen müsstest. Je nach Anwendungsfall könnte das zu viel Overhead sein.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Funktion für mehrere Klassen

Beitrag von Alexander Kornrumpf »

Oder von vornherein keine Vererbung verwenden. Oder ...

Ist sehr schwer eine beste Lösung vorzuschlagen wenn alles was wir über das Problem wissen ein Spielzeugausschnitt ist, der nicht kompiliert.

Danke deswegen an Jonathan, der das Problem nochmal geschärft hat.
joeydee
Establishment
Beiträge: 1039
Registriert: 23.04.2003, 15:29
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von joeydee »

Wenn ich richtig verstanden habe ... das prinzipielle Problem ist ja, alles irgendwie mit einer Stammklasse gemeinsam verarbeiten zu können, aber auf der anderen Seite auch mit bestimmten Komponenten (möglichst jederzeit und überall) wieder spezialisieren können, und das möglichst ohne Overhead. Sowas wie "Für alle Tiere: wenn du eine Ente bist ..." ohne ein Verbot auf Enten an dieser Stelle zu bekommen, und ohne dass man alle Eventualitäten für alle Tiere bereithalten müsste, was bei 3 Enten unter 1000 Tieren ja Quatsch wäre.
Hier z.B. klingt das auch ganz ähnlich: viewtopic.php?f=4&t=4390

Vielleicht lohnt es ja, mal Data-Driven statt Object-Oriented als alternative Designlösung anzuschauen. In dem Fall auch unter "ECS", Entity-Component-System, zu finden. Bringt natürlich neue Probleme mit sich. Ist aber inzwischen mein Favorit bei dieser Art von Problemen.

Der wesentliche Unterschied zu OO ist:
OO bündelt alle Eigenschaften für ein Objekt in einem gemeinsamen "Container" (Klasse).
DD bündelt jeweils eine Eigenschaft für alle Objekte in einem gemeinsamen Container (Key-Value-Map).
Methoden in Objekten gib es dann auch nicht, Funktionalität für bestimmte Strukturen wird über Schleifen bzw. Filter und Ergebnislisten geregelt.

Sieht dann etwa so aus:

Code: Alles auswählen

//es gibt Tiere und anderes, manche sind als Ente bekannt, wer "Energy" hat kann generell gefüttert werden
map <uint, int> energies
map <uint, bool> ducks

//eine Ente
uint e=new Entity()
energies[e]=15
ducks[e]=true

//ein Fisch
e=new Entity()
energies[e]=3

//ein Stein
e=new Entity()
//andere Eigenschaften...


//------

//alle füttern
for(key e in energies) energies[e]++

//nur Enten füttern
for(key e in ducks) if(energies[e]!=null) energies[e]++
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Jonathan hat geschrieben: 21.09.2020, 20:47 Alternativ könntest du natürlich vector<Tier*> übergeben.

Falls das Problem noch nicht klar geworden ist: Im Vector liegen alle Objekte im Speicher hintereinander. Eine Ente ist aber unter Umständen größer als ein Tier, deshalb kann man ein vector<Ente> nicht als vector<Tier> interpretieren. Ggf. einfach mal das Memory-Layout aufmalen, dann sieht man es direkt.
Zeiger sind aber immer gleich groß, durch diese Indirektion tritt das Problem dann nicht mehr auf.
Danke für die Ausführung, das war mir tatsächlich nicht klar! Ist aber absolut schlüssig.
joeydee hat geschrieben: 22.09.2020, 12:48 Wenn ich richtig verstanden habe ... das prinzipielle Problem ist ja, alles irgendwie mit einer Stammklasse gemeinsam verarbeiten zu können, aber auf der anderen Seite auch mit bestimmten Komponenten (möglichst jederzeit und überall) wieder spezialisieren können, und das möglichst ohne Overhead. Sowas wie "Für alle Tiere: wenn du eine Ente bist ..." ohne ein Verbot auf Enten an dieser Stelle zu bekommen, und ohne dass man alle Eventualitäten für alle Tiere bereithalten müsste, was bei 3 Enten unter 1000 Tieren ja Quatsch wäre.
Hier z.B. klingt das auch ganz ähnlich: viewtopic.php?f=4&t=4390

Vielleicht lohnt es ja, mal Data-Driven statt Object-Oriented als alternative Designlösung anzuschauen. In dem Fall auch unter "ECS", Entity-Component-System, zu finden. Bringt natürlich neue Probleme mit sich. Ist aber inzwischen mein Favorit bei dieser Art von Problemen.

Der wesentliche Unterschied zu OO ist:
OO bündelt alle Eigenschaften für ein Objekt in einem gemeinsamen "Container" (Klasse).
DD bündelt jeweils eine Eigenschaft für alle Objekte in einem gemeinsamen Container (Key-Value-Map).
Methoden in Objekten gib es dann auch nicht, Funktionalität für bestimmte Strukturen wird über Schleifen bzw. Filter und Ergebnislisten geregelt.
Jetzt verstehe ich die Idee! Wirklich raffiniert! Vielen Dank!
Wieso nimmst du dafür eine map? Verliert man nicht sehr viel Zeit, wenn man erst nach einem Element mittels Schlüssel suchen muss, ehe man darauf zugreifen kann? Wäre ein direkter Zugriff über einen std::vector nicht wesentlich schneller (das schnellste?)?
Alexander Kornrumpf hat geschrieben: 22.09.2020, 08:56 Oder von vornherein keine Vererbung verwenden. Oder ...

Ist sehr schwer eine beste Lösung vorzuschlagen wenn alles was wir über das Problem wissen ein Spielzeugausschnitt ist, der nicht kompiliert.

Danke deswegen an Jonathan, der das Problem nochmal geschärft hat.
Das stimmt schon, diese Situation ist nur eine Analogie. In meinem Code gehts tatsächlich um etwas anderes. Ich wollte fürs bessere Verständnis Irrelevantes ausklammern.
Aber vielleicht sind diese Informationen doch relevant.

Es geht halt darum, dass ich in meiner Kollisionsabfrage das behandelnde Elemente auf die restlichen prüfen muss und dieses eine Element auch interne Werte der anderen Elemente beeinflussen kann (z.B die velocity).
Ich habe also ein ausgewähltes Objekt A , mit welchem ich Berechnungen zu jedem Element einer Gruppe machen muss { B, C, D, E}.
Diese Gruppe ist nur ein Auszug einer noch grösseren Gruppe. Es sind die Elemente die mir mein Broad-Phase Algorithmus zurückgeliefert hat (Vorselektion).
Nun muss A zu jedem Element der Gruppe bestimmte Dinge berechnen, wie z.B. die Distanz. Einmal ausgerechnet, muss ich aber für diesen Zyklus darauf zugreifen können (heisst, solange bis Element B als Kandidat an der Reihe ist).
Nun ist es aber so, das B - wie A selbst - vielleicht ein Kreis ist. Hingegen C eine Wand-Line. Bei der Wand-Linie gibt es zwar auch eine Distanz, welche berechnet werden muss, im Vergleich zu einem Kreis, gibt es dann aber noch weitere Werte zu berechnen.

Ich wollte nicht, dass all diese Hilfsvariablen wie z.B. die Distanz ein fester Bestandteil der Klasse sind. Das würde die Klasse stark aufblähen und ich müsste sie nach gebrauch wieder manuell resetten. Das scheint mir einfach kein gutes Design zu sein.
Ich hab mir daher überlegt, den entsprechenden Klassen einfach einen Zeiger zu geben, der quasi auf ein temporäres "Datasheet" zeigt. Auf diesem sind alle Hilfsergebnisse gespeichert - und das ganze Sheet wird gelöscht, nachdem Objekt A behandelt wurde.
Bildlich gesprochen: Objekt A tackert Element B, C, D, E ein Zettel an die Stirn und schreibt darauf die relevanten Rechenergebnisse, wie z.B. die Distanz. Wenn A fertig ist, werden die Zettel wieder entfernt.

Auf was anderes komm ich irgendwie nicht :-X

Gruss starcow
Freelancer 3D- und 2D-Grafik
mischaschaub.com
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Funktion für mehrere Klassen

Beitrag von Chromanoid »

Bullet und sicher auch andere Engines dieser Art lösen das glaube ich so, dass es für jede relevante Art von Kollision Kreis vs. Kreis, Linie vs. Kreis etc. eigene "Algortihmus-Klassen" gibt, die Kollisionserkennung in der "Narrow-Phase" übernehmen. Daraus entstehen dann Kontaktpunkte (deine Datenblätter?) (siehe z.B. http://jbullet.advel.cz/javadoc/com/bul ... ifold.html), die durch einen Solver dann aufgelöst werden (z.B. http://jbullet.advel.cz/javadoc/com/bul ... olver.html).
joeydee
Establishment
Beiträge: 1039
Registriert: 23.04.2003, 15:29
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von joeydee »

Wieso nimmst du dafür eine map? Verliert man nicht sehr viel Zeit, wenn man erst nach einem Element mittels Schlüssel suchen muss, ehe man darauf zugreifen kann? Wäre ein direkter Zugriff über einen std::vector nicht wesentlich schneller (das schnellste?)?
Ich (in meinem Programm) nehme dafür keine Map. Aber ich zeige hier das grundlegende DD-Prinzip anhand einer Map, weil es so am einfachsten geht. Und für viele Spiele würde das sogar schon reichen, könnte ich wetten.
Wenn du in einem Vector den Key als Index nimmst und besagte 3 Enten die IDs 213, 899 und 912 haben, gewinnt man ja nichts beim Durchiterieren für die Anfrage "Für alle Enten ...". Das wäre dann 1000 schnelle vs. 3 langsame Zugriffe, und das Problem skaliert mit der Gesamtmenge. Die Map dagegen nicht. Durch eine Komponente iterieren, das 90% aller Entities haben, verhält es sich dagegen umgekehrt. So einfach ist die Lösung also auch nicht. Das gehört dann unter besagte "neue Probleme". Geht aber, ich hatte da mal eine Lösung mit einem entsprechend verwalteten Dense-Vector. Aber das bleibt dann nicht das einzige Problem.

Irgendeinen Tod muss man bei sowas immer sterben, was random Querzugriffe angeht. Für mich spielt dann Bedienbarkeit, Erweiterbarkeit, Wartbarkeit des ganzen Systems die entscheidendere Rolle, danach Implementierungsaufwand. Und weniger, ob ich so mehrere 100.000 oder vielleicht nur 10.000 Entities beherrschen kann die sich gegenseitig checken (AI gehört ja später auch noch ins Boot). Das wäre zwar beeindruckend, ist aber i.d.R. gar nicht notwendig für schicke One-Man-Hobbygames.
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Ok, dann wird es wohl Zeit, dass ich mich damit auseinander setze!
Was nimmst du denn, in deinem Programm? Ein vector?

Eine Frage noch:
Ist es erlaubt sowas zutun? Oder mach ich hier was verbotenes? Er kompiliert jedenfalls anstandslos.
Wenn es erlaubt ist, würde das mir ein Problem lösen (-:

Code: Alles auswählen

// Die Basis-Notizen - haben alle Tiere
class CLS_Calc {
public:
	int x;
};

// erweiterte Notizen - haben nur gewisse Tiere
class CLS_ECalc : public CLS_Calc {
public:
	int y;
};

class CLS_Tier {
public:
	CLS_Calc* Calc;
};

class CLS_Ente : public CLS_Tier {
};

int main(int argc, char** Argv) {
	CLS_Ente ente;
	ente.Calc = new CLS_ECalc;
	ente.Calc->x = 13;
	// ente.Calc->y = 8; // Das geht nicht

	CLS_ECalc* zeiger = static_cast<CLS_ECalc*>(ente.Calc); // Aber das geht! Darf man das?
	zeiger->y = 8;

	std::cout << zeiger->x << "\n";
	std::cout << zeiger->y << "\n";

	delete ente.Calc;
	ente.Calc = nullptr;

	return 0;
}
Merkt das Programm denn, dass an ente.Calc eine ECalc Instanz dran hängt und löscht somit auch den ganzen Block?
Weil in der Basisklasse Tier ist ja eigentlich ein Zeiger des Typs "Calc" - und nicht "ECalc" deklariert.

Gruss starcow
Freelancer 3D- und 2D-Grafik
mischaschaub.com
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Schrompf »

static_cast zur Ableitung hin darfst Du, wenn Du sicher bist, dass es auch wirklich eine Instanz dieser Ableitung ist. Das passt so.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
joeydee
Establishment
Beiträge: 1039
Registriert: 23.04.2003, 15:29
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von joeydee »

Wenn der Cast dein Problem löst und Schrompf sagt du darfst das, bleib dran :)
Wenn es um relativ geschlossene Systeme wie Physik geht, passt ein Klassensystem noch relativ gut.

ECS hat seine Stärken vor allem dann, wenn man ständig neue Komponenten erfindet, die zudem nicht so richtig in Hierarchien passen (wer ist die gemeinsame Basis Char.inventar und Box.Inventar, wenn nur wenige Chars und Kisten ein Inventar benötigen?), oder Altlasten wieder ausklammern will. Typisch Hobbyprogger eben, beim Tippen erst die Spielidee entwickeln ;)

Ja, bei mir liegen die Datenpakete in einem dense Vector zum schnellen Durchiterieren. Für den Lookup component->entityId gibts einen parallel laufenden Vector (alternativ, wenn man ausschließlich eigene Komponenten verwendet, erbt man alles von einer Basis mit Eigenschaft entityId). Für den Lookup entityId->component kann man je nach Geschmack eine Map (wäre dann auch rel. dense) oder einen sparse Vector machen der so lang ist wie die Komponente mit der höchsten ID in der Liste.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Jonathan »

Aber wieso static_cast? Scheint so, als sei ein dynamic cast hier sehr viel angebrachter:

https://en.cppreference.com/w/cpp/language/dynamic_cast

Wenn die Basisklasse mindestens eine virtuelle Funktion enthält, macht der dynmaic_cast einen Laufzeitcheck, ob der Basisklassenzeiger auch wirklich auf die erwartete Unterklasse zeigt, wenn dies nicht der Fall ist, kriegst du einen 0-Zeiger zurück (siehe 5 c) in obigen Link). Ansonsten wird dein Programm einfach irgendwo abstürzen, wenn diese Garantie einmal nicht gegeben ist.
Wenn du ansonsten wild Zeiger umbiegen möchtest, würde ich das eher mit einem reinterpret_cast machen, da sieht man dann direkter, dass etwas potentiell gefährliches im Code passiert. Wundert mich ehrlich gesagt leicht, dass es mit static_cast überhaupt funktioniert.

Wie gesagt: dynamic_cast ist die beste Möglichkeit um dein Programm sicher zu machen. Dieses "ich bin mir aber sicher, dass das so an dieser Stelle passt" kann man schon machen, es sollte dann aber wirklich am besten im Rahmen einer einzelnen Funktion oder ähnlichem geschehen. Ansonsten ist es gut möglich dass du die Garantie jetzt geben kannst, das Programm später aber irgendwann erweitert wird und es dann irgendwann mal knallt.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Schrompf »

Jonathan hat geschrieben: 26.09.2020, 09:56 Aber wieso static_cast? Scheint so, als sei ein dynamic cast hier sehr viel angebrachter:

https://en.cppreference.com/w/cpp/language/dynamic_cast

Wenn die Basisklasse mindestens eine virtuelle Funktion enthält, macht der dynmaic_cast einen Laufzeitcheck, ob der Basisklassenzeiger auch wirklich auf die erwartete Unterklasse zeigt, wenn dies nicht der Fall ist, kriegst du einen 0-Zeiger zurück (siehe 5 c) in obigen Link). Ansonsten wird dein Programm einfach irgendwo abstürzen, wenn diese Garantie einmal nicht gegeben ist.
Nuja, wie ich schon schrieb: wenn Du weißt, dass es diese konkrete Instanz ist, dann geht static_cast. dynamic_cast ist auch eine nützliche Sache, aber halt nicht für den Fall, dass Du die Instanz kennst.
Wenn du ansonsten wild Zeiger umbiegen möchtest, würde ich das eher mit einem reinterpret_cast machen, da sieht man dann direkter, dass etwas potentiell gefährliches im Code passiert. Wundert mich ehrlich gesagt leicht, dass es mit static_cast überhaupt funktioniert.
Das ist schlicht falsch. Der Compiler haut Dir das mit reinterpret_cast sogar um die Ohren, glaube ich.
Wie gesagt: dynamic_cast ist die beste Möglichkeit um dein Programm sicher zu machen. Dieses "ich bin mir aber sicher, dass das so an dieser Stelle passt" kann man schon machen, es sollte dann aber wirklich am besten im Rahmen einer einzelnen Funktion oder ähnlichem geschehen. Ansonsten ist es gut möglich dass du die Garantie jetzt geben kannst, das Programm später aber irgendwann erweitert wird und es dann irgendwann mal knallt.
Das ist ne gute Zusammenfassung. Wenn man es nicht sicher weiß und trotzdem static_castet, dann knallt's oder korrumpiert subtil die Daten im Speicher. Gefährlich. dynamic_cast ist massiv viel langsamer, aber ein paar Dutzend Millionen davon kriegst Du trotzdem pro Sekunde durch.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Funktion für mehrere Klassen

Beitrag von Spiele Programmierer »

Würde auch beim static_cast bleiben. dynamic_cast ist dramatisch langsamer und das um so mehr desto komplizierter deine Klassenhierarchie ist. Es macht auf dem MSVC-Compiler sogar string-Vergleiche, das heißt, man sollte kurze Klassennamen benutzen! Hier ein schneller GCC-Benchmark.

reinterpret_cast macht es aber nur noch gefährlicher, da nun Casts möglich sind, die gar keinen Sinn ergeben. Niemand will hier "wild Zeiger umbiegen". Wenn du z.B. von Klasse A nach Klasse B konvertieren möchtest, und A und B nichts miteinander zu tun haben, dann deutet das eher auf einen Tippfehler hin. Und mit static_cast sind derartige Tippfehler ausgeschlossen:

Code: Alles auswählen

class A { ... };
class B { ... };
const B& test(const A& a)
{
    return static_cast<const B&>(a); // Compiler-Fehler
}
Ich bin mir jetzt auch nicht sicher, ob reinterpret_cast laut C++-Standard theoretisch überhaupt als static_cast-Ersatz erlaubt ist. Auf jeden Fall aber gibt es bei Mehrfachvererbung einen großen praktischen Unterschied:

Code: Alles auswählen

class A { ... };
class B { ... };
class C : public A, public B {};
const C& test(const B& b)
{
    //return reinterpret_cast<const C&>(b); // Kein Compiler-Fehler, es folgt höchstwahrscheinlich ein Programm-Absturz!
    return static_cast<const C&>(b); // Okay
}
Ich denke man muss auch immer schauen um welche Art Anwendung es sich handelt und welche Sicherheit man anstrebt. In einem Computerspiel ohne Netzwerkzugriff ist es zum Beispiel fast egal, ob die Anwendung durch die weiteren Folgen eines falschen Cast mit static_cast oder durch einen Null-Pointer-Zugriff nach dem dynamic_cast abstürzt. Das zweitere spart potentiell ein paar Minuten beim Debuggen aber ansonsten gibt es keine "Gefahr" in dem Sinn.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Krishty »

Spiele Programmierer hat geschrieben: 26.09.2020, 14:25Ich bin mir jetzt auch nicht sicher, ob reinterpret_cast laut C++-Standard theoretisch überhaupt als static_cast-Ersatz erlaubt ist. Auf jeden Fall aber gibt es bei Mehrfachvererbung einen großen praktischen Unterschied:
Nein; deine Demo ist Undefined Behavior. Niemand sagt, dass Basis und Ableitung die selbe Adresse haben – genau das spuckt reinterpret_cast aber nunmal aus. Du greifst via reinterpret_cast unter der falschen Adresse auf die Ableitung zu.

Siehe auch diese lustige Mikrooptimierung: viewtopic.php?f=11&t=2501#p36621
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
starcow
Establishment
Beiträge: 523
Registriert: 23.04.2003, 17:42
Echter Name: Mischa Schaub
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von starcow »

Danke für die umfangreiche Hilfe. Finde die vielen guten Inputs einmal mehr sehr spannend zu lesen! :-)

Es ist tatsächlich so, dass durch die Programmstruktur garantiert ist, dass ein "Tier-Zeiger" nur dann in einen "Enten-Zeiger" gecastet wird, wenn das entsprechende Objekt auch wirklich eine "Ente" ist. Die Daten liegen entsprechend sortiert vor, so dass das Ganze wasserdicht ist.
Da dieser Programmteil zeitkritisch ist, bin ich froh, wenn die Casts so schnell wie möglich erledigt sind. dynamic_cast scheint mir daher auch ein unnötiger Resourcenfresser zu sein.

Eine Sache ist mir jedoch noch nicht klar.

Code: Alles auswählen

Class Tier {};
Class Ente  : public Tier {};

void func(std::vector<Tier*>& Tier_VEC) {return;}

std::vector<Ente*> Enten_VEC;
func(Enten_VEC); // Geht leider nicht
Eigentlich müsste das jetzt doch gehen...? Beide Vektoren beinhalten blos Zeiger, welche in beiden Fällen auch gleich viel Platz benötigen.
Zudem kann man mit einem "Tier-Zeiger" ja nichts anstellen, was man mit einem "Enten-Zeiger" nicht auch könnte.
Andersrum würde es ja durchaus Sinn ergeben, dass sowas nicht zulässig wäre. Aber so rum, wie in meinem Fall?
An was liegts genau?

Gruss
starcow
Freelancer 3D- und 2D-Grafik
mischaschaub.com
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Funktion für mehrere Klassen

Beitrag von Spiele Programmierer »

Sehr gut beobachtet. Leider geht das in C++ nicht. Nichtmal in einem ganz einfachen Fall wie diesem:

Code: Alles auswählen

class A {};
class B : public A {};
A** cast(B** b) { return static_cast<A**>(b); }
Nach dem cast kann man natürlich auch *cast(b_ptr_ptr) = new A() setzen, sodass der ursprüngliche b_ptr_ptr jetzt ein Objekt enthält, das nicht vom Typ B ist. Analog wie du in deinem Code in func ein Tier zu der Liste hinzufügen könntest, das keine Ente ist.

Ich persönlich finde diese Limitierung von C++ nicht sehr sinnvoll. Durch static_cast hat man ja eig. schon so viel wie "ich pass schon auf" zum Compiler gesagt.

Wahrscheinlich ist der eigentlich Grund warum das nicht unterstützt wird, irgendwie irgendwas mit Freiheit bei der Implementierung, Mehrfachvererbung, was wenn Zeigertypen unterschiedlich groß sind und anderer hypothetischer Müll. Fakt ist auf jeden Fall, dass es mit reinterpret_cast auf allen mir bekannten Implementierungen theoretisch geht, aber leider gibt es da keine Unterstützung für von Seiten des C++-Standards (und dadurch könnten z.B. durch Strict Aliasing Probleme auftreten).

Fazit: Ich fürchte, die leider einzige Möglichkeit das Problem gut zu umgehen, ist es, überall std::vector<Tier*> zu verwenden.

@Krishty
Hm, das es bei Mehrfachvererbung nicht gehen kann, ist klar (wie auch in meinem zweiten Beispiel demonstriert). Ich bin mir aber nicht sicher, wie es bei Einfachvererbung aussieht, aber wahrscheinlich hast du recht und es ist auch nicht garantiert. In C++ ist nichts garantiert.

Und selbst dann kann man sich noch die Unterfrage stellen wie es bei Typen mit standard Layout aussieht. Rein theoretisch müsste der C++ Standard doch hier das richtige (und kompatible) Speicherlayout garantieren. Wahrscheinlich ist es aber streng genommen trotzdem noch Undefined Behaviour wegen Objekt-Lebenszeiten und so ein Krampf. Habe gerade mal die von mir selbst verlinkte Quelle durchgelesen. Darin steht zwar, dass der reinterpret_cast-Anwendungsfall für solche Typen erlaubt ist, aber auch, dass Standard-Layout-Typen entweder in der Basisklasse oder abgeleiteten Klasse keine Datenmember definieren dürfen. Also nutzlos.

EDIT: Fazit & Korrektur am Ende
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Schrompf »

Ihr seid da beide aufm falschen Dampfer. Tier und Ente sind verwandte Typen. std::vector<Tier*> ist aber nicht verwandt mit std::vector<Ente*>. Ihr könnt euch Template-Typen vorstellen, als wären alle Template-Params Teil des Namens: vector_voll_mit_TierPtrn ist halt eine andere Klasse als vector_voll_mit_EntePtrn. Das geht in C++ nicht und auch in Java oder C# nicht.

Deine Beispiele sind bisher halt ein bisschen konstruiert. Ist ja auch gut so bisher. Aber hier kann man jetzt schlecht diskutieren, wie eigentlich die richtige Lösung dafür wäre. Ableitungen sind super, wenn Du viele verschiedene Ausprägungen eines Typs mit dem selben Griff anfassen willst. Dein Griff ist hier Tier und Ausprägungen davon sind dann Ente, Giraffe oder Delphin. Was Du über den Griff tun willst, weiß ich aber nicht. Eine Methode Aufscheuchen() wäre denkbar, aber viel mehr Aktionen, die für all diese Tierarten gemeinsam funktionieren, fallen mir gar nicht ein. Das ist der Punkt, wo man bemerkt, dass Ableitungen vielleicht doch nicht die geeignete Lösung sind.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Funktion für mehrere Klassen

Beitrag von Spiele Programmierer »

Mir ist klar das es aktuell in C++ nicht geht. Aber das ist ja nur so wie die Sprache halt nunmal aktuell ist. Rein prinzipiell wäre es schon möglich das zu unterstützen und es würde auch sehr viel Sinn ergeben. Im Falle von einfachen Zeigern könnte die Sprache das einfach direkt erlauben und im Falle von std::vectoren durch entsprechende Kooperation mit der STL ebenfalls.

Insbesondere für konstante Parameter (z.B. in einem array_view/span) hät ich das auch schon ab und zu brauchen können und da wäre es sogar ein absolut sicherer Cast.

In Java wird der Anwendungsfall, ja wie oben im Thread schon erwähnt, durch List<? extends Tier> unterstützt.
Und auch in C# geht es für Arrays und bei generischen Parametern die mit out gekennzeichnet sind:

Code: Alles auswählen

class A { int a; }
class B : A { int b; }
class Program
{
    static void Main(string[] args)
    {
        A[] t = new B[10];
        IReadOnlyList<A> t2 = new List<B>();
    }
}
Da C++ im Gegensatz zu C# oder Java keine sichere Sprache bezüglich Speicherverwaltung ist, behaupte ich sogar, dass die Implementierung dort tendentiell unproblematischer sein sollte. Im Hintergrund passiert nur ein reinterpret_cast.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Krishty »

Spiele Programmierer hat geschrieben: 30.09.2020, 22:07Insbesondere für konstante Parameter (z.B. in einem array_view/span) hät ich das auch schon ab und zu brauchen können und da wäre es sogar ein absolut sicherer Cast.

[…]

Da C++ im Gegensatz zu C# oder Java keine sichere Sprache bezüglich Speicherverwaltung ist, behaupte ich sogar, dass die Implementierung dort tendentiell unproblematischer sein sollte. Im Hintergrund passiert nur ein reinterpret_cast.
Nur bei einzigartiger Basis (siehe Unterschied reinterpret_cast vs. static_cast oben). Bei mehr als einer Basisklasse muss deine span möglicherweise kopiert und von allen kopierten Zeigern 8 Bytes abgezogen werden. Spätestens da brechen alle Konzepte zusammen (denn span soll keine Speicherverwaltung betreiben).

Wenn man das haben will spricht doch auch nichts gegen

Code: Alles auswählen

template <typename U, typename V> std::vector<V> static_cast_elements(std::vector<U> const & other) {
    std::vector<V> result;
    result.reserve(other.size);
    for(auto const & u : other) {
        result.emplace_back(static_cast<V>(u));
    }
    return result;
}

void func(std::vector<Tier*>& Tier_VEC);

std::vector<Ente*> Enten_VEC;
func(static_cast_elements<Tier *>(Enten_VEC));
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Krishty »

Wieder was auf Old New Thing gelernt: Das Problem ist die Wahl zwischen Invariance, Covariance, and Contravariance. Der Link erklärt die Konzepte schön; und Old New Thing erklärt schön, dass C# das anders als C++ macht und welche Probleme das bei den WinRT-C++-Wrappern aufwirft.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Jonathan
Establishment
Beiträge: 2352
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Funktion für mehrere Klassen

Beitrag von Jonathan »

Ok, wieder was gelernt. Um es zusammen zu fassen:

Für das casten von Basis-Klasse auf abgeleitete Klasse:
- reinterpret_cast: Deaktiviert einfach die Typprüfung und nimmt die Speicheradresse wie sie ist. Kann bei komplexen Klassenhierarchien ungültige Zeiger erzeugen, dann knallt es. Für diesen Anwendungsfall nie eine gute Idee.
- static_cast: Prüft ob die Klassen kompatibel sind (man kann also keine zwei vollkommen unabhängigen Klassen casten?) und biegt gegebenenfalls die Zeiger korrekt um. Funktioniert, wenn man sich sicher ist, dass man ein Objekt der abgeleiteten Klasse hat.
- dynamic_cast: Führt eine Laufzeitprüfung durch und ist damit die sicherste Variante. Allerdings auch die langsamste.

Es wurde angesprochen, dass dynamic_cast zuweilen sehr ineffizient sein kann. Wie ist dann die best practise? Ist es wirklich besser z.B. in der Basisklasse ein enum einzubauen, anhand dessen man entscheiden kann um welche abgeleitete Klasse es sich handelt und basierend darauf einen static_cast zu machen?
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Antworten