Search code examples
c++ooppolymorphism

c++ derived class narrows member type


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.


Solution

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

    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.

    Better Solution - Polymorphic Classes

    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?