I have the following scenario:
struct A { void f(); };
struct B : A { void g(); };
struct Base {
A &ref;
Base(A &a) : ref(a) {}
void f() { ref.f(); }
};
struct Derived : Base {
Derived(B &b) : Base(b) {}
// ERROR: ref does not have function g() since its stored as an A& in Base
void h() { ref.g() }
};
My question is how I can best represent what I'm trying to represent without making an extra duplicate reference. For example, one proposed solution is to add a member B& ref2
in Derived
but that would mean that we are storing an extra A&
in Base
since the new member has all the functionality of ref
.
Another solution I thought of is to change A& ref
to A* ptr
in Base
and use static_cast<B*>(ptr)
in Derived
. However, this feels fragile because in the future someone might change the constructor of Derived
to have an argument that is not a B
Is there a better solution? I have the ability to modify all classes in my scenario, so I have all the flexibility needed.
Another solution I thought of is to change
A& ref
toA* ptr
inBase
and usestatic_cast<B*>(ptr)
inDerived
. However, this feels fragile because in the future someone might change the constructor ofDerived
to have an argument that is not aB
.
You don't have to store A
as a pointer, you can also static_cast
between references. However, you probably want to use pointer members anyways, because the assignment operators of your class won't be deleted that way.
The solution you've described is fragile, but we can make it less fragile by creating a type alias in Derived
:
struct Base {
A *ptr; // store a pointer to avoid headaches with ref members
Base(A &a) : ptr(&a) {}
void f() { ptr->f(); }
};
struct Derived : Base {
using ActualType = B;
Derived(ActualType &b) : Base(b) {}
void h() {
static_cast<ActualType*>(ptr)->g();
}
};
With this type alias, we can keep the type used inside of h
in sync with the constructor.
The first solution is still very dirty, because we are downcasting to ActualType*
, and that's still a bit of a footgun. It would be better if we didn't have to do that at all.
We can make A
and B
polymorphic classes:
// note: A needs a virtual destructor if we ever destroy a B by calling the
// destructor of A
struct A {
void f();
virtual void g() = 0; // note: pure virtual, might need an implementation in A
// otherwise A is an abstract class
};
struct B : A {
void g() override { /* ... */ }
};
// ...
struct Derived : Base {
Derived(B &b) : Base(b) {}
// note: virtual call of A::g(), will dynamically dispatch to B::g()
void h() { ptr->g(); }
};
In general, if you find yourself downcasting, this is usually an indicator that you should have used polymorphism instead.
See also: When to use virtual destructors?