Envy
Envy

Reputation: 113

Overload function with different arguments

Hope you can help me out with this. Consider the following class hierarchy:

class Collider
{
  public:
  ... //Some functions that aren't worth mentioning. They all are abstract so the class is abstract.
}

class CircleCollider : public Collider
{
  private:
  vec2 pos_;
  float radius_;

  public:
  ... //The definition of the parent functions and constructor / destructor.
}

class RectCollider : public Collider
{
  private:
  vec2 pos_;
  vec2 size_;

  public:
  ... // Again, the definition of the parent functions and constructor / destructor.
}

In another class I've got a pointer to the parent class that creates a new instance of one of the two child classes, depending on an enum value, like so:

void SetCollisionType(ECollisionType Type)
{
  switch(Type)
  {
    case CIRCLE:
    {
       Collider* collider = new CircleCollider(pos, radius);
       break;
    }
    case RECTANGLE:
    { 
       Collider* collider = new RectCollider(pos, size);
       break;
    }
  }
}

Please note I've simplified the function so you get an idea of how my code works. Now what I want to do is a function that updates the member variables of each Child class, radius for CircleCollider, and size for RectCollider. I thought of adding a function to the Collider class:

virtual void UpdateCollider(float NewRadius, vec2 NewSize) = 0;

And then define it in each child class.

   void CircleCollider::UpdateCollider(float NewRadius, vec2 NewSize)
   { 
     radius_ = NewRadius;
   }

   void RectCollider::UpdateCollider(float NewRadius, vec2 NewSize)
   {
     size_ = NewSize;
   }

The problem I see with this is that the definition used in CircleCollider will not use the NewSize parameter, and the same will happen to NewRadius with the definition in RectCollider. However, I cannot think of another way to do it. Do you know another way to do it making use of the hierarchy and polymorphism present in this code? Thank you very much in advance!

Upvotes: 2

Views: 100

Answers (4)

Bitwize
Bitwize

Reputation: 11230

Although the other answers address ways that this could work, I would be inclined to take a step back and consider your abstractions. From a design perspective, if you find yourself in a situation where parameters are only orthogonal for different implementations, then you may not have chosen an effective abstraction from the start.

You should consider whether the UpdateCollider needs to be virtual at all. Judging by the enum of ECollisionType, this appears to be a fixed number of types. In which case, you could re-design your Collider implementations such that each of them have public member functions that satisfy their goals, e.g.:

class CircleCollider : public Collider
{
private:
   // ...

public:
   void setRadius(...);
}

class RectCollider : public Collider
{
private:
  // ..

public:
  void setSize(...);
}

And then, rather than updating in some Collider::UpdateCollider abstract function, you can do the update from outside (such as in some ColliderManager) by casting to the correct underlying type, and calling the specific setters required for that type. There are two possible ways I can think of to do this:

  • using dynamic_cast to poll for the correct type (creates an if/else if ladder)
  • using your ECollisionType to designate a program invariant, which assumes that ECollisionType is always accurately specifying the correct Collider type -- then using static_cast to the correct type (note: This is technically less-safe if ever your invariant isn't upheld, since it could lead to Undefined Behavior -- but would be faster since it doesn't require querying RTTI).

For example:

// Using dynamic_cast
void ColliderManager::dynamicColliderUpdater(Collider& c) {
  if (auto* p = dynamic_cast<CircleCollider&>(&c) {
    p->setRadius(...);
  } else if (auto* p = dynamic_cast<RectCollider&>(&c) {
    p->setSize(...);
  } 
  ...
}

// Using static cast
void ColliderManager::staticColliderUpdater(Collider& c, EColliderType t)
{
  switch (t) {
    case CIRCLE: {
      static_cast<CircleCollider&>(c).setRadius(...);
    }
    case RECTANGLE: {
      static_cast<RectCollider&>(c).setSize(...);
    }
    ...
  }

}  

Whether this approach works or not would depend entirely on your design. If you intend to allow consumers to extend and add Collider types, then this likely isn't an effective approach. However if it's always fixed and internalized, then a good abstraction is to have a *Manager (such as ColliderManager) take care of this. The concern of the manager would be updating, and managing all possible collider types -- which doesn't violate any abstraction by having it be aware of the appropriate underlying type. This approach can also allow more complex setters for more complex colliders, and the ColliderManager would be responsible for massaging the data so that it coherently works for the respective Collider

Upvotes: 0

R Sahu
R Sahu

Reputation: 206717

When you have classes that need very different kinds of data to construct and update, the only thing that can be commonly used to construct and update objects of those types is a std::string or a stream-like object. That will allow you the generality that can be supported at the base class and the specificity needed by the derived classes.

void SetCollisionType(ECollisionType Type,
                      std::istream& is)
{
  switch(Type)
  {
    case CIRCLE:
    {
       float radius;
       is >> radius;
       Collider* collider = new CircleCollider(pos, radius);
       break;
    }
    case RECTANGLE:
    { 
       vec2 size;
       // Assuming such an overload exists.
       is >> size;
       Collider* collider = new RectCollider(pos, size);
       break;
    }
  }
}

and

void CircleCollider::UpdateCollider(std::istream& is)
{ 
   is >> radius_;
}

void RectCollider::UpdateCollider(std::istream& is)
{
   is >> size_;
}

It's easy to construct a std::istringstream from a std::string and pass them to the above functions.

Upvotes: 1

user10957435
user10957435

Reputation:

I would recommend using std::variant, if you have access to C++17 or later.

Of course, a poor man's std::variant would probably be a union:

union UpdateArg {
    float NewRadius;
    vec2 NewSize;
};
//...
void RectCollider::UpdateCollider(UpdateArg NewSize) {
    //...
}

Of course, if you really want to make it extendable for other things, not just circles and rectangles, you also have std::any.

Upvotes: 2

tdao
tdao

Reputation: 17713

You can use a base class function accepting a pointer to Collider as an input parameter. Then use dynamic_cast to determine the type of the Collider object and update the parameter accordingly.

Something like this,

void Collider::UpdateCollider(Collider *p, float NewRadius, vector NewSize)
{
    if(CircleCollider *p_circle_collider = dynamic_cast<CircleCollider *>(p))
    {
        // this is a CircleCollider object
        p_circle_collider->radius_ = NewRadius;
    }
    else if( RectCollider *p_rect_collider = dynamic_cast<RectCollider *>(p))
    {
        // this is a RectCollider object
        p_rect_collider->size_ = NewSize;
    }
}

Of course, you'll need some accessor for radius_ and size_ to compile, or else you need them as public members.

Upvotes: 1

Related Questions