I'm trying to design an file system for a personal game engine I'm working on and I have a hard time using polymorphism while avoiding dynamic casting.
I have different kind of objects in my game engine (e.g., Texture
, Mesh
, etc.) which all inherits from the class Object
.
class Object
{
};
class Texture : public Object
{
};
I have the concept of files which holds an object.
class File
{
public:
Object* GetObject() const { return m_Object; }
private:
Object* m_Object;
};
I want to be able to define a command for a file holding a specific type of object, hence the class FileCommand
.
class FileCommand
{
public:
virtual void Execute(File* file) = 0;
};
Now, let's say we have a class called OpenFileInTextureEditorCommand
which inherits from FileCommand
. This class should obviously expect to receive a file containing a texture. But, to ensure that, we have to cast the return value of file.GetObject()
, which is of type Object*
, to Texture*
.
class OpenFileInTextureEditorCommand : public FileCommand
{
public:
void Execute(File* file) override
{
Texture* texture = std::dynamic_cast<Texture*>(file->GetObject());
if (texture)
{
// Open texture in texture editor...
}
}
};
The best I've been able to achieve with my designs is to camouflage the dynamic casting.
So, let's re-use the class Object
and the class Texture
Now, I've transformed the class File
into a templated class. Since files needs to be stored in a std::vector
, I'm forced to create a non-templated base class FileBase
. The purpose of File
is mainly to handle dynamic casting.
class FileBase
{
public:
Object* GetObject() const { return m_Object; }
protected:
Object* m_Object = nullptr;
};
template<class T>
class File : protected FileBase
{
public:
// Turns the class into an abstract class -- cannot be instanciated. Its only job to handle dynamic casting.
virtual ~File<T>() = default;
T* GetObject() const
{
// Camouflaged dynamic casting
return dynamic_cast<T*>(m_Object);
}
};
Since my goal is to avoid dynamic casting in my FileCommand
implementations, I've also transformed it into a templated class. Since FileCommand
also needs to be stored in a std::vector
, I'm forced to create a non-templated base class FileCommandBase
. As with File
, the purpose of FileCommand
is mainly to handle dynamic casting.
class FileCommandBase
{
public:
virtual void Execute(FileBase* file) = 0;
};
template<class T>
class FileCommand : protected FileCommandBase
{
public:
// Turns the class into an abstract class -- cannot be instanciated. Tts only job is to handle dynamic casting.
virtual void Execute(File<T>* file) = 0;
protected:
void Execute(FileBase* file) override
{
// Camouflaged dynamic casting
Execute(dynamic_cast<File<T>*>(file));
}
};
Finally, we can implement OpenFileInTextureEditorCommand
, which doesn't require the dynamic casting anymore.
class OpenFileInTextureEditorCommand : public FileCommand<Texture>
{
public:
void Execute(File<Texture>* file) override
{
Texture* texture = file.GetObject();
if (texture)
{
// Open texture in texture editor...
}
}
};
The code of OpenFileInTextureEditorCommand
in itself isn't really shorter, but I think it's cleaner.
Although, while writing this post, I've come to some conclusions:
Now, I may be too persistent on trying to avoid to do dynamic casting, but I see people talking about how the presence of dynamic casting is an indicator of a bad design.
Thanks for reading!
In my opinion:
First, dynamic_cast
is a keyword instead of a method, so std
is not needed.
Second, if the Object
itself isn't virtual, static_cast
is enough.
Third, we say "dynamic casting is an indicator of bad design" because you may abstract them as a common class with all necessary methods to show their properties so that polymorphism will handle them. However, Object
is too high-level, too abstract, that it will obviously hide many unique characteristics of the object and it's hard to perform polymorphism suitably. So in that case, i.e. you have some very general classes, and you rely on them heavily, dynamic casting is totally acceptable. This is a usual case in more OOP languages, e.g. in C#, it's called boxing and unboxing to represent cast to object
or vice versa.
Finally, dynamic_cast
may have a not-bad performance if there is no multiple inheritance; in the best case, it may be only slightly slower than static_cast
, so it may not be a big concern if you really need it(unless your profiler tells you not...). You can see the analysis in this blog.