Seite 1 von 2
Funktion für mehrere Klassen
Verfasst: 18.09.2020, 17:17
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
Re: Funktion für mehrere Klassen
Verfasst: 18.09.2020, 17:44
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
Re: Funktion für mehrere Klassen
Verfasst: 18.09.2020, 18:36
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?
Re: Funktion für mehrere Klassen
Verfasst: 18.09.2020, 19:44
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
Re: Funktion für mehrere Klassen
Verfasst: 18.09.2020, 20:08
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.
Re: Funktion für mehrere Klassen
Verfasst: 18.09.2020, 20:20
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.
Re: Funktion für mehrere Klassen
Verfasst: 19.09.2020, 08:41
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.
Re: Funktion für mehrere Klassen
Verfasst: 20.09.2020, 12:11
von starcow
Vielleicht etwas doofe Frage, doch was muss ich mir denn unter "generisch" Vorstellen?
Re: Funktion für mehrere Klassen
Verfasst: 20.09.2020, 14:12
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.
Re: Funktion für mehrere Klassen
Verfasst: 20.09.2020, 20:45
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;
}
Re: Funktion für mehrere Klassen
Verfasst: 21.09.2020, 20:47
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.
Re: Funktion für mehrere Klassen
Verfasst: 22.09.2020, 08:56
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.
Re: Funktion für mehrere Klassen
Verfasst: 22.09.2020, 12:48
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]++
Re: Funktion für mehrere Klassen
Verfasst: 23.09.2020, 15:43
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
Re: Funktion für mehrere Klassen
Verfasst: 23.09.2020, 16:45
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).
Re: Funktion für mehrere Klassen
Verfasst: 24.09.2020, 11:26
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.
Re: Funktion für mehrere Klassen
Verfasst: 25.09.2020, 16:51
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
Re: Funktion für mehrere Klassen
Verfasst: 25.09.2020, 16:58
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.
Re: Funktion für mehrere Klassen
Verfasst: 26.09.2020, 08:53
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.
Re: Funktion für mehrere Klassen
Verfasst: 26.09.2020, 09:56
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.
Re: Funktion für mehrere Klassen
Verfasst: 26.09.2020, 13:21
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.
Re: Funktion für mehrere Klassen
Verfasst: 26.09.2020, 14:25
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.
Re: Funktion für mehrere Klassen
Verfasst: 26.09.2020, 16:05
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
Re: Funktion für mehrere Klassen
Verfasst: 30.09.2020, 14:07
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
Re: Funktion für mehrere Klassen
Verfasst: 30.09.2020, 15:14
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
Re: Funktion für mehrere Klassen
Verfasst: 30.09.2020, 20:52
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.
Re: Funktion für mehrere Klassen
Verfasst: 30.09.2020, 22:07
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.
Re: Funktion für mehrere Klassen
Verfasst: 30.09.2020, 23:05
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));
Re: Funktion für mehrere Klassen
Verfasst: 09.10.2020, 22:53
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.
Re: Funktion für mehrere Klassen
Verfasst: 16.10.2020, 10:57
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?