Reputation: 113
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
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:
dynamic_cast
to poll for the correct type (creates an if
/else if
ladder)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
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
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
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