Jonathan hat geschrieben: ↑16.10.2020, 10:57Es 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?
Wenn das geht am besten die Objekte gar nicht mischen, das ist besonders was Performance anbelangt unübertroffen. Selbst wenn du die Objekte nur via virtuellen Funktionen zugreifst (ohne
dynamic_cast), sind gemischte Objekte sehr ungünstig da die Sprungvorhersage jedesmal falsch liegen wird. Das so zu organisieren, dass die Objekte nicht gemischt werden, läuft wohl unter dem Schlagwort Entity-Component-Systeme. Wobei ich den Begriff nicht sonderlich mag, da sich das so kompliziert und starr anhört.
Manchmal ist es aber auch einfacher, die Objekte wenigstens gemeinsam abzuspeichern. Die Möglichkeit mit der Enumeration besteht sicherlich und das hab ich auch schon öfter gesehen. Wenn man es erweiterbar halten will, kann man auch einen Zeiger nehmen. Also z.B. so:
Code: Alles auswählen
struct BaseIdentifier {};
struct Base
{
BaseIdentifier const* Identifier;
};
struct Derived : public Base
{
static const BaseIdentifier Identifier; // Wobei man hier in der Quelldatei auch noch die Variable definieren muss. Achtung bei "constexpr" außerhalb von Klassen, das kann mehrfach instanziert werden sodass der Zeiger zu einem solchen "Identifier" nicht mehr eindeutig ist.
Derived()
: Base { &Identifier }
{}
};
Dieses Vorgehen hat den Vorteil (wenn man dies braucht!), dass man auch in unabhängigen Code noch Klassen hinzufügen kann. Z.B. sogar in Plugin-DLLs. Außerdem geht es mir oft so, dass ich gerne noch ein paar weitere Meta-Eigenschaften zu den Klassen abspeichern möchte. Zum Beispiel den Namen, einen Zeiger zu einer Funktion die eine frische Instanz der Klasse erstellt, und ggf. weitere solche Daten die man in anderen Sprachen teilweise auch über Reflection bekäme. Solche Dinge kann man prima mit in die
BaseIdentifier-Klasse im obigen Beispiel packen und damit beispielsweise eine Liste aller verschiedenen Klassen erstellen.
Ich habe bei mir oft RTTI deaktiviert, was man für
typeid und
dynamic_cast braucht. Teilweise ist das aber ehrlich gesagt inzwischen auch eher aus Gewohnheit. In Code der nicht performancekritisch ist, spricht meiner Meinung nach nicht grundsätzlich etwas gegen diese Sprachmittel. Selbst wenn man eine Liste unbekannter Objekte hat, sollte man ja nach Möglichkeit auch eh nicht so irre viel rumkonvertieren sondern wenn dann eher virtuelle Funktionen aufrufen.
Schrompf hat geschrieben: ↑16.10.2020, 12:41
reinterpret_cast ist außerdem auf Typen der gleichen Speichergröße beschränkt. Du kannst nen Pointer in einen anderen Pointer casten. Du kannst ne
struct { float a, b, c; } in eine
struct { int x, y, z; } casten. Aber Du kannst auch mit nem
reinterpret_cast nicht einen MemberFunctionPtr in einen FunctionPtr konvertieren. Und auch keinen
double in einen
float.
Hm, ich weiß nicht wie du das meinst. Also das geht ja z.B. nicht:
Code: Alles auswählen
struct s { float a, b, c; };
struct t { int x, y, z; };
t cast(s obj) { return reinterpret_cast<t>(obj); }
Soweit ich weiß geht
reinterpret_cast nur für alle Zeiger- und Referenztypen. Und dann ist die Größe egal.
Ein wichtiger Haken ist noch, dass man dann nicht auf die umgebogenen Zeiger zugreifen darf (außer es sind
char-Zeiger-Typen zu denen du konvertierst), zumindest wenn man dem C++-Standard treu sein will. Das ist diese Strict-Aliasing-Regel.
reinterpret_cast ist zum Beispiel dann die richtige Wahl, wenn du irgendwo eine Struktur hast (z.B. in einer Bibliothek) und du deinen Zeiger darin speichern willst. Dann kannst du den Zeiger erst nach
void* konvertieren, in der Struktur abspeichern und dann zu einem späteren Zeitpunkt mit
reinterpret_cast wieder zu einem Zeiger zu deinem Typ zurück konvertieren und verwenden. Das ist erlaubt. Was nicht erlaubt ist, ist z.B. einen
float-Zeiger in einen
int32_t-Zeiger umzuwandeln
und dann zuzugreifen. Das ist nur mit
memcpy oder, teilweise, mit
union erlaubt.