2
votes

I have a global vector of unique_ptrs to a base class, to which I append unique_ptrs to a derived class:

std::vector<std::unique_ptr<Base>> global_vec;

template<typename T>
Base* create_object()
{
  std::unique_ptr<T> uptr = std::make_unique<T>(/* ... */);
  Base* last_ptr = uptr.get();
  global_vec.emplace_back(std::move(uptr));
  return last_ptr; /*this is a bit irrelevant, but is for the caller*/
}

Now, Base itself has a member vector of raw pointers to Base:

struct Base
{
  ...
  std::vector<Base*> providers;
  ...
}

The pointers that make up Base::providers are all obtained via calling unique_ptr::get() from global_vec:

void Base::subscribe_to(Base* src)
{
  providers.push_back(src);
}

Base has a member function that does work with these subscribers, and checks for nullptr before doing work:

void Base::do_work()
{
  ...
  for(Base* ptr : providers)
  {
    if(ptr != nullptr)
    {
      ...
    }
  }
}

Now, somewhere else in my code, I can erase the unique_ptrs in global_vec:

auto itr = std::find_if(global_vec.begin(), global_vec.end(), [&](std::unique_ptr<Base> const& n)
        { return n.get() == ptr_selected; }); //ptr_selected points to a widget selected by the user
  global_vec.erase(itr);

However, after erasing an element, Base::suscribers will still hold a valid pointer to an object. That is, when iterating through Base::providers in Base::do_work(), no Base* will equal std::nullptr.

I would expect that erasing a unique_ptr from global_vec would invoke Base::~Base(), thus rendering the pointers in Base::providers as std::nullptr. Base's destructor is invoked, but the pointers are valid (you can even get access to data members from them).

Base does have a virtual destructor.

Why are the pointers in Base::providers still valid?

1
get returns by value!Kerrek SB
Destroying an object has no effect on pointers to that object except making dereferencing them undefined. And undefined may seem to work, occasionally (there is no way to determine whether a pointer is valid).molbdnilo
Pointers are one way. There is no backchannel to update other pointers to the same object. The book-keeping to implement such a channel would be impractical.user4581301

1 Answers

5
votes

I would expect that erasing a unique_ptr from global_vec would invoke Base::~Base()

Yes, it does.

thus rendering the pointers in Base::providers as std::nullptr.

This is where your expectation fails. There is no way for raw pointers to an object to be set to nullptr automatically when the object is destroyed. It is YOUR responsibility to handle that manually in your own code. You need to remove a Base* pointer from the providers vector before/when the corresponding object is destroyed. The compiler cannot do that for you.

You might consider having two vector<Base*> in your Base class, one to keep track of objects that this has subscribed to, and one to keep track of objects that have subscribed to this. Then, ~Base() can unsubscribe this from active subscriptions, and notify active subscribers that this is going away. For example:

struct Base
{
...
protected:
    std::vector<Base*> providers;
    std::vector<Base*> subscribers;
    ...

public:
    ~Base();

    ...

    void subscribe_to(Base* src);
    void unsubscribe_from(Base* src);

    ...
};

Base::~Base()
{
    std::vector<Base*> temp;

    temp = std::move(providers);
    for(Base* ptr : temp) {
        unsubscribe_from(ptr);
    }

    temp = std::move(subscribers);
    for(Base* ptr : temp) {
        ptr->unsubscribe_from(this);
    }
}

void Base::subscribe_to(Base* src)
{
    if (src) {
        providers.push_back(src);
        src->subscribers.push_back(this);
    }
}

void Base::unsubscribe_from(Base* src)
{
    if (src) {
        std::remove(providers.begin(), providers.end(), src);
        std::remove(src->subscribers.begin(), src->subscribers.end(), this);
    }
}

void Base::do_work()
{
    ...
    for(Base* ptr : providers) {
        ...
    }
    ...
}

...

std::vector<std::unique_ptr<Base>> global_vec;

Otherwise, consider using std::shared_ptr instead of std::unique_ptr in your global vector, and then you can store std::weak_ptr<Base> objects instead of raw Base* pointers in your other vectors. When you access a std::weak_ptr, you can query it to make sure the associated object pointer is still valid before using the pointer:

struct Base
{
...
protected:
    std::vector<std::weak_ptr<Base>> providers;
    ...
public:
    ...

    void subscribe_to(std::shared_ptr<Base> &src);

    ...
};

void Base::subscribe_to(std::shared_ptr<Base> &src)
{
    if (src) {
        providers.push_back(src);
    }
}

void Base::do_work()
{
    ...
    for(std::weak_ptr<Base> &wp : providers) {
        std::shared_ptr<Base> ptr = wp.lock();
        if (ptr) {
            ...
        }
    }
    ...
}

...

std::vector<std::shared_ptr<Base>> global_vec;

Base's destructor is invoked, but the pointers are valid

No, they are not valid, since the object being pointed to was destroyed. The pointers are simply left dangling, pointing to the old memory, and are not nullptr like you are expecting.

you can even get access to data members from them

It is undefined behavior to access members of an object after it has been destroyed.