1

Recently, I have been abstracting and cleaning up a project of mine to get it ready for new features. I came across a design problem when I realized that I wanted to manage pointers to a graphics context and a window as abstract classes. I have searched for answers but haven't found anything satisfying yet. I have a situation where I'm creating a window that extends the window class and the graphics context class like so...

class base_a
{
// base_a virtual methods
};

class base_b
{
// base_b virtual methods
};

class derived : public base_a, public base_b
{
//other methods
};

int main ()
{
    derived* d = new derived();
// This is what I want to do
    std::shared_ptr<base_a> ptr1 (d);
    std::shared_ptr<base_b> ptr2 (d);
}

While this more or less works during the lifetime of the object, it becomes a problem when destructing since depending on the order of destruction you are potentially deleting empty memory. It's useful to have the pointers to both as I need access to the virtual functions and I would like to keep the graphics context and the window decoupled if possible. Is there a way to do this?

Edit: Changed unique_ptr to shared_ptr as the former was incorrect

Sara W
  • 127
  • 1
  • 8
  • 1
    Why not just `std::unique_ptr d { std::make_unique() };`? You can then call methods from `base_a` and `base_b` like that: `d->baseAMethod()`, `d->baseBMethod()` – op414 Jul 13 '21 at 16:27
  • 1
    use `base_a* ptr1 (d); base_b* ptr1 (d);`? – NathanOliver Jul 13 '21 at 16:28
  • @op414 That would work but I don't really want the code that deals with the graphics context to know anything about the window, since it is not guaranteed that the it will have a class that derives both. It might be what I end up doing though. I might also make the graphics context a subclass of window, though that doesn't really line up with the idea of the graphics context being a feature of the window. – Sara W Jul 13 '21 at 16:32
  • You can do this then: `std::unique_ptr d { std::make_unique() }; base_a* ptr1 { static_cast(d.get()) }; base_b* ptr2 { static_cast(d.get()) };` you can then pass `ptr1` and `ptr2` to other functions/methods which only care about one interface. What is important is to have a single `std::unique_ptr` to manage the lifecycle of your object. – op414 Jul 13 '21 at 16:40
  • 5
    You can't have two `unique_ptr` to the same object. This should have been obvious. You can have two `shared_ptr` to the same object. This should have been obvious. What's less obvious is how to make two `shared_ptr` of different type to the same object that share a reference count, but `shared_ptr` is designed to allow that. `shared_ptr` has support for pointers to subobjects keeping the complete object alive, and that support can be used for either base subobjects or member subobjects. – Ben Voigt Jul 13 '21 at 16:43
  • @BenVoigt That is true, however this is a bit of a weird case as they are pointers to to base classes. When I use a `unique_ptr`, this will point to the place within the derived class that the base class exist thus you can have unique pointers like that though it is obviously error prone. I did not know though that `shared_ptr` had that functionality. Is there anyway to prevent other classes from owning them i.e. enforcing something like a `weak_ptr`? – Sara W Jul 13 '21 at 17:00
  • 1
    @JustinW: Just because the pointers compare unequal does not mean they are unique for the purposes of `unique_ptr`. `unique_ptr` is one-per-object, and always points at the complete object that needs to be destroyed and deallocated when the `unique_ptr` goes out of scope. The code in your question was already wrong when you made one `unique_ptr`, the second one was just piling on. – Ben Voigt Jul 13 '21 at 18:12
  • @BenVoigt oh ok I see. I will update the question – Sara W Jul 13 '21 at 18:13

1 Answers1

5

Use a shared_ptr to manage ownership of the derived class. Then use std::shared_ptr's aliasing constructor to share ownership of the derived instance through a pointer to one of the base classes. Here's what that might look like:

class base_a {
public:
    int x = 1;
};

class base_b {
public:
    int y = 2;
};

class derived : public base_a, public base_b {
public:
    int z = 3;
};

int main() {
    auto d_ptr = std::make_shared<derived>();
    
    auto a_ptr = std::shared_ptr<base_a>(d_ptr, static_cast<base_a*>(d_ptr.get()));
    auto b_ptr = std::shared_ptr<base_b>(d_ptr, static_cast<base_b*>(d_ptr.get()));
    ...

This provides safe access to the base classes without risking early or double deletion. Only when the last of all these pointers goes out of scope will the derived instance (and both of it base class objects) be destroyed.


std::cout << "x : " << d_ptr->x << '\n';
std::cout << "y : " << d_ptr->y << '\n';
std::cout << "z : " << d_ptr->z << '\n';

std::cout << "---\n";
a_ptr->x = 4;
b_ptr->y = 5;

std::cout << "x : " << d_ptr->x << '\n';
std::cout << "y : " << d_ptr->y << '\n';
std::cout << "z : " << d_ptr->z << '\n';

Output:

x : 1
y : 2
z : 3
---
x : 4
y : 5
z : 3

Live Demo


EDIT: while the above holds true for member objects and base class sub-objects, shared_ptr already defines conversions for base class pointers, as was kindly pointed out by @BenVoigt. This simplifies the code to:

auto d_ptr = std::make_shared<derived>();

auto a_ptr = std::shared_ptr<base_a>(d_ptr);
auto b_ptr = std::shared_ptr<base_b>(d_ptr);

Updated Live Demo

alter_igel
  • 6,899
  • 3
  • 21
  • 40
  • 1
    This isn't wrong, but `shared_ptr` defines conversions that will set up the link for you in the case of base subobjects. You'd have to do exactly this in the case of member subobjects. – Ben Voigt Jul 13 '21 at 18:09