Search code examples
c++castingarchitecturepolymorphismgame-engine

C++ - Hard Time With Polymorphism And Dynamic Casting


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.

Problem

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...
        }
    }
};

Solution

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...
        }
    }
};

Conclusion

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:

  • More dynamic casting than before, except it's implicit in some way
  • More classes than before, may be harder to maintain

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!


Solution

  • 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.