2

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.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
yasgur99
  • 756
  • 2
  • 11
  • 32
  • 1
    *without making an extra duplicate reference* -- Note that none of your code actually uses references. You are passing everything by-value. In all honesty, this looks like a java or python programmer's attempt at C++, believing that references are passed. – PaulMcKenzie Jul 05 '23 at 20:55
  • There are [quite a few errors](https://godbolt.org/z/4xevj1PWq) in this code. Did you try compiling what you are showing here? – Drew Dormann Jul 05 '23 at 21:02
  • @PaulMcKenzie i've updated the post, adding the references I've omitted. – yasgur99 Jul 05 '23 at 21:02
  • @DrewDormann I've fixed compiler errors, except the one I point out – yasgur99 Jul 05 '23 at 21:04
  • @yasgur99 should `class C` be entirely private? I'm surprised that you're not getting multiple errors from this code. – Drew Dormann Jul 05 '23 at 21:07
  • @DrewDormann, that wasn't meant to be the case. Post updated to make it public. – yasgur99 Jul 05 '23 at 21:08
  • An `A` object knows nothing about `B`, thus this design cannot work. Once you started to pass `A` references around in the constructor for `C`, it's game over. – PaulMcKenzie Jul 05 '23 at 21:12
  • @PaulMcKenzie, my question is whether there is a better solution than to change A and B to pointer members and use static cast to resolve this problem. – yasgur99 Jul 05 '23 at 21:20
  • Is there a reason why you cannot have `g()` be a virtual function of `A`? – PaulMcKenzie Jul 05 '23 at 21:22
  • @PaulMcKenzie, I could, but there is no reason for `A` to need to know about `g`. – yasgur99 Jul 05 '23 at 21:26
  • @yasgur99 So to boil this all down, you want a way to say "Call g() if it exists as a member function, otherwise don't do anything?" – PaulMcKenzie Jul 05 '23 at 21:27
  • @PaulMcKenzie I want to say "Derived class should be able to call ref.g() but Base class should not be able to call ref.g()." – yasgur99 Jul 05 '23 at 21:28

3 Answers3

2

It compiles by adding virtual void g() to A.

Alternative using virtual method:

struct A { void f(); virtual void g(); };
struct B : A { void g() override; };

struct Base {
  A &ref;
  Base(A &a) : ref(a) {}
  void f() { ref.f(); }
};

struct Derived : Base {
  Derived(B &b) : Base(b) {}
  // Works: because B::g() overrides A::g()
  void h() { ref.g(); }
};

Alernative with template:

struct A { void f(); };
struct B : A { void g(); };

template<typename TElem>
struct Base {
  TElem &ref;
  Base(TElem &a) : ref(a) {}
  void f() { ref.f(); }
};

struct Derived : Base<B> {
  Derived(B &b) : Base(b) {}
  // Works: because ref is a B
  void h() { ref.g(); }
};
Betaloid
  • 31
  • 4
  • This is a good solution, but my concern here is that `A` does not need to know that there is some function `g` in its derived classes. – yasgur99 Jul 05 '23 at 21:23
  • Another alternative would be to make a template of Base, but it all depends on how you want to use class Base and Derived. – Betaloid Jul 05 '23 at 21:35
  • *'method'* is not a C++ term as by the standard, this one only speaks of functions and *member* functions (though suffices *'virtual function'* as only member functions can be virtual anyway...). – Aconcagua Jul 06 '23 at 10:00
2

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?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • Just a minor detail: If A needs a virtual destructor or not doesn't depend on where it is stored – (if storage at all then where deriving classes are stored, but that's still not the relevant point), but if at some point derived classes get deleted via pointers to base... – Aconcagua Jul 06 '23 at 09:57
  • @Aconcagua fair point, I've clarified this a bit in the answer and added a link to a post with more detail. It's not so easy to put it into simple terms when A needs a virtual destructor tbh, it involves a lot of standardese lingo – Jan Schultke Jul 06 '23 at 10:02
  • Though polymorphism has some implications on its own, e.g. `A` and `B` get larger due to having include the vtable pointer and function calls get slightly slower due to an additional level of indirection. So *'better'* gets relativised a bit, generally true, but in special cases (limited space, but many objects or high performance code) not suitable. – Aconcagua Jul 06 '23 at 10:06
  • About the virtual destructor (again...): I usually adhere strongly the C++ principle *'don't pay for what you don't need'* – at the VD I allow myself an exception: We already have the vtable anyway, and if we can afford the issues mentioned before anyway we normally can for the destructor as well. So I'd always add one and would only remove again on failing some requirements elsewhere. – Aconcagua Jul 06 '23 at 10:12
  • *'if we ever destroy a B by calling the destructor of A'* – `B* b = ...; b->~A();` ??? Still unlucky wording... Maybe: *'delete a `B` via `A*` pointer'*? – Aconcagua Jul 06 '23 at 10:18
  • @Aconcagua that's still not accurate. You also run into this issue when you manually manage lifetimes and call destructors directly. It's not exclusive to `delete`. – Jan Schultke Jul 06 '23 at 10:45
0

I suggest using polymorphism (virtual methods), but as an alternative, you could add helper functions in a class between Base and Derived. This class would then know what type of A that ref is referencing. Here, a function that does the proper cast to B& is provided:

template<class T>
struct BaseT : Base {
    T& Ref() { return static_cast<T&>(ref); }
};

struct Derived : BaseT<B> {
    Derived(B& b) : BaseT{b} {}

    void h() { Ref().g(); }
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108