westem
westem

Reputation: 63

How to avoid circular references with member callbacks & shared_ptr?

In our codebase, we have a plethora of cases like the following:

auto f = std::make_shared<Foo>();
auto b = std::make_shared<Bar>();
//void Foo::DoStuff( std::shared_ptr<Foo>, int ) 
auto func = std::bind( &Foo::DoStuff, f, std::_1 );
b->RegisterFunc( func );

Obviously, pocketing the shared_ptr in a stored callback is generally a bad idea, as it oft causes f to never be destructed. Normally, you can just pass in a weak_ptr and check whether it is still valid on execution.

Unfortunately, DoStuff() modifies internal state of the Foo object, and is a member function, so it requires the pointer to be valid to avoid crashes. Which causes circular references, and requires manual intervention like calling Foo::Shutdown() or something to remove them.

Is there a way to work around the circular references storing callbacks of member functions?

Note: For the purposes of the question, I am requiring the bound function to be a non-static member function. It is too ubiquitous in our code to justify completely reworking every example, even if it is clearly a design oversight.

Upvotes: 0

Views: 408

Answers (1)

Ayjay
Ayjay

Reputation: 3433

As you say, you have to use weak_ptr and check that it's valid. weak_ptr can be used to create valid pointers to the object, so you can still call DoStuff() on pointers obtained from a weak_ptr. It's not clear to me why you think weak_ptr will not work.

Here's your example, but now it's showing a potential circular reference that's avoided with use of weak_ptr

#include <memory>
#include <functional>
#include <vector>
#include <cassert>

struct Foo {
    int x = 0;
    using callback_t = std::function<void()>;
    std::vector<callback_t> callbacks;

    void DoStuff() { x = 1; } //mutate some state
    void RegisterFunc(callback_t callback) { callbacks.push_back(std::move(callback)); }
};

using Bar = Foo;

template <class T>
auto wrap_shared_ptr_callback(const std::shared_ptr<T>& p, void(T::*f)()) {
    return [p = std::weak_ptr{ p }, f]() {
        auto ptr = p.lock();
        if (ptr) (*ptr.*f)();
    };
}

void create_circular_reference(std::shared_ptr<Foo> f, std::shared_ptr<Bar> b) {
    auto f_callback = wrap_shared_ptr_callback(f, &Foo::DoStuff);
    b->RegisterFunc( f_callback );
    auto b_callback = wrap_shared_ptr_callback(b, &Bar::DoStuff);
    f->RegisterFunc( b_callback );

    // test callbacks
    b->callbacks.front()();
    f->callbacks.front()();
}

int main() {
    auto f = std::make_shared<Foo>();
    auto b = std::make_shared<Bar>();

    // keep weak ptrs so we can check they've been destroyed correctly
    auto f_w = std::weak_ptr{ f };
    auto b_w = std::weak_ptr{ b };

    create_circular_reference(std::move(f), std::move(b));

    assert(f_w.expired());
    assert(b_w.expired());
}

Upvotes: 1

Related Questions