Corelli
Corelli

Reputation: 21

How can I access to derived members from a pure virtual base class function?

I want to have a Collider interface class in which will have a overloaded -> operator to have access directy to the BoxCollider derived class. I want to have access to the members of box collider through the interface and chnage the type of collider at run-time. So I thought of using templates:

template<typename T>
class ColliderV2 {
public:
    virtual T* operator ->() = 0;
};

class BoxColliderV2 : public ColliderV2<BoxColliderV2> {
public:
    float width;
    float height;

    BoxColliderV2* operator ->() {
        return this;
    }


};
int main()
{
    ColliderV2<BoxColliderV2>* col = new BoxColliderV2;
    (*col)->width = 1; 

}

This works. But templates , as far as I know, will generate a brand new Collider class in compile-time filling T with Box Collider, correct? Thats why it worked. But later it prevents me from changing the collider type. I also thought of just making a virtual Collider class with Collider* operator->() ; overload in the derived class BoxCollider* operator->() ;

But if I tried :

Collider<BoxCollider>* col = new BoxCollider;
(*col)->width = 1; // won't work

doesn't work since Collider is not BoxCollider. And I don't want to dynamic_cast every possible collider type I could have. So, what can be done here?

Upvotes: 0

Views: 67

Answers (3)

Doeus
Doeus

Reputation: 420

I think you are approaching the problem from the wrong direction. The purpose of an interface is that you don't have to know about the exact type or the implementation.

For example: You are using Axis-Aligned Bounding Boxes for collision detection. So, even if your CircleCollider uses a radius, you are still able to calculate its width and height from it. Now, you don't have to worry about if you are dealing with a BoxCollider or a CircleCollider, you have everything to make a Bounding Box.

class Collider
{
public:
    virtual float x() const = 0;
    virtual float y() const = 0;
    virtual float width() const = 0;
    virtual float height() const = 0;
};

class BoxCollider : public Collider
{
    // Implementation...
};

class CircleCollider : public Collider
{
    // Implementation...
};

Of course, you are maybe using something else, and not AABBs. I just wanted to demonstrate how you can use interfaces effectively.

Upvotes: 0

Tiger4Hire
Tiger4Hire

Reputation: 1091

What you are trying to do is discouraged by C++. What you are trying to do is to change the type of something based on the return value of a function. The type system is designed to stop you from writing code like this.
One important restriction of a function is that can only return one type-of-thing. You can return one of a list of things if you wrap those possibilities in a class, and return that. In C++17, a ready-made class for this is std::variant. The restriction on this is that the list of things must be fixed (or a closed-set). If you want an arbitrary set of return values (open-set), you must use a different approach. You must restate your problem in terms a function that is done on the return value.

class BoxColliderV2 : public MyBaseCollider {
public:
    void SetWidth(float new_width) override; 
};

You may find this video useful. The bit of interest starts at around 40 minutes (but watch the whole video if you can). If you are interested in advice, I would suggest starting with std::variant, and if it works, move to virtual functions. Problems like collision detection get really complicated really quickly, and you will almost certainly require double dispatch at some stage. Start simple, because it's only going to get more complicated.

These excerpts from the ISO-Guidelines may help
1. When you change the semantic meaning of an operator, you make it harder for other programmers to understand you code. guideline.
2. Dynamic casting is verbose and ugly, but deliberately so, because dynamic casting is dangerous, and should stand out. guideline

Upvotes: 0

Timo
Timo

Reputation: 9835

As you've already found out, this doesn't work. Templates and runtime behavior are kind of contradicting mechanics. You can't create a common base class and let it act like a generic pointer to give you access to its derived types' members.

An interface specifies a contract against which you can code. You don't code against a specific implementation but the interface, so the interface has to provide all the members that you'd like to access. In your case this would result in width and height beeing part of ColliderV2 instead of BoxColliderV2. However this defeates the logic you are trying to mimic.

There are a few approaches that you can take:

  1. Either make your collider type a variant, like

    using ColliderType = std::variant<BoxColliderV2, MyOtherCollider, ...>;
    

    and check for the actual type when you want to access the member

    ColliderType myCollider = /* generate */;
    
    if (auto boxCollider = std::get_if<BoxColliderV2>(&myCollider); boxCollider)
        boxCollider->width = 0;
    
  2. Or, keep the base class that you have, remove the operator-> and the template and do a dynamic cast on it:

    ColliderV2* col = new BoxColliderV2;
    if (auto boxCollider = dynamic_cast<BoxColliderV2*>(col); boxCollider)
        boxCollider->width = 0;
    
  3. You can also hide details like width or height behind more generic functions that are part of the interface. For example:

    class ColliderV2 {
    public:
        virtual void setBounds(float width, float height) = 0;
    };
    
    class BoxColliderV2 : public ColliderV2 {
    public:
        void setBounds(float width, float height) override {
            this->width = width;
            this->height = height;
        }
    
    private:
        float width;
        float height;
    };
    
    int main()
    {
        ColliderV2* col = new BoxColliderV2;
        col->setBounds(1, 1); 
    }
    

Upvotes: 2

Related Questions