2
\$\begingroup\$

It is not always possible to simplify program design by strictly managing the lifetimes of objects. If two objects have unpredictable lifetimes, but one of them needs to refer to the other, a simple pointer or reference is a segmentation fault waiting to happen.

The approach that I tried first was to have both objects contain a pointer to the other, and code in the destructors of both objects that would set the other object's pointer to null:

#include <iostream>

class Manager;

class Tracked
{
public:
    ~Tracked();
    void act() {std::cout << "hello world!\n";}
    void setManager(Manager *manager) {manager_ = manager;}
    void markManagerInvalid() {manager_ = nullptr;}
private:
    Manager *manager_;
};

class Manager
{
public:
    void attach(Tracked &tracked)
    {
        tracked_ = &tracked;
        tracked_->setManager(this);
    }
    void detach()
    {
        if(tracked_ != nullptr)
        {
            tracked_->markManagerInvalid();
            tracked_ = nullptr;
        }
    }
    void invoke()
    {
        if(tracked_ != nullptr)
            tracked_->act();
    }
private:
    Tracked *tracked_;
};

Tracked::~Tracked()
{
    if(manager_ != nullptr)
        manager_->detach();
}

int main()
{
    Manager manager;
    {
        Tracked tracked;
        manager.attach(tracked);
        manager.invoke(); //output: hello world!
    }//"tracked" goes out of scope: the pointer in "manager" is now invalidated.
    manager.invoke();
}

This works and sounds very simple but in practice it has serious drawbacks:

  • There is not only tight coupling but also a circular dependency between Tracker and Manager.

  • In order to have many Manager instances link to the same Tracked instance, the Tracked class would have to incorporate a list of Manager pointers to instances that are linking to it.

  • The code is complex and error prone, and would get even worse with multiple instances linking to each other.

To get around this I devised a different solution: A dynamically allocated pointer to the Tracked instance. The pointer would be set to nullptr by the pointed Tracked instance in the insance's destructor to mark the instance as expired. The same pointer instance is shared using std::shared_ptr with all objects requiring safe access to the Tracked instance.

#include <iostream>
#include <memory>

class Tracked
{
public:
    using Handle = std::shared_ptr<Tracked*>;
    Tracked() : handle_(new Tracked*(this)) { }
    ~Tracked() {*handle_ = nullptr;}
    void act() {std::cout << "hello world!\n";}
    Handle getHandle() {return handle_;}
private:
    Handle handle_;
};

class Manager
{
public:
    void attach(Tracked &tracked) {handle_ = tracked.getHandle();}
    void detach() {handle_.reset();}
    void invoke()
    {
        if(handle_ != nullptr)
        {
            if(*handle_ != nullptr)
                (*handle_)->act();
        }
    }
private:
    Tracked::Handle handle_;
};

int main()
{
    Manager a, b, c;
    {
        Tracked tracked;
        a.attach(tracked);
        b.attach(tracked);
        a.invoke(); //output: hello world!
    } //"tracked" goes out of scope: the handles in "a" and "b" are now invalidated.
    b.invoke();
    c.invoke();
}

I have found surprisingly little information on this, is this a known idiom or design pattern?

Edit: At the conclusion of writing this question I realized that I could just allocate the Tracked object with new and manage the object with a std::shared_ptr, distributing a std::weak_ptr to each Manager. If std::weak_ptr::lock() would return a null shared_ptr, the Tracked instance would be expired.

\$\endgroup\$
  • 2
    \$\begingroup\$ Aye. But look at make_shared, it is potentialy more eficient \$\endgroup\$ – Deduplicator May 14 '14 at 22:09
  • 3
    \$\begingroup\$ I can't say that this is recognized as a pattern, but it is a major use case for std::shared_ptr and std::weak_ptr. \$\endgroup\$ – YoungJohn May 14 '14 at 22:21
2
\$\begingroup\$

I think using a bit of dynamic allocation std::shared_ptr and std::weak_ptr makes sense as they provide a solid implementation of the tracking behaviour you need.

Your original though has a nice property of not using any dynamic allocation. If the tracking logic could be refine and made more general if might be a handy bit of library code to have around.

This is my attempt so far:

template <typename T>
class SafeRefTo;

template <typename T>
class Tracked : public T
{
public:
    using T::T;
    ~Tracked();
    void addReferee(SafeRefTo<T> *manager) {safeRefToThis = manager;}
    void markSafeRefInvalid() {safeRefToThis = nullptr;}
private:
    SafeRefTo<T> *safeRefToThis;
};

template <typename T>
class SafeRefTo
{
public:
    template <typename... Args>
    Tracked<T> makeTracked(Args... args)
    {
        Tracked<T> tracked{args...};
        attach(tracked);
        return tracked;
    }

    void attach(Tracked<T> &tracked)
    {
        tracked_ = &tracked;
        tracked_->addReferee(this);
    }
    void detach()
    {
        if(tracked_ != nullptr)
        {
            tracked_->markSafeRefInvalid();
            tracked_ = nullptr;
        }
    }
    template <typename TrackedMemFun, typename... Args>
    void invoke(TrackedMemFun fun, Args... args)
    {
        if(tracked_ != nullptr)
            (tracked_->*fun)(args...);
    }
private:
    Tracked<T> *tracked_;
};

template <typename T>
Tracked<T>::~Tracked()
{
    if(safeRefToThis != nullptr)
        safeRefToThis->detach();
}

Then the client code is simply:

#include <iostream>

class Actor
{
public:
    Actor() = default;
    Actor(std::string extraText)
    :
    extraText(extraText)
    {}

    void act(unsigned int times) {
        for(; times; --times)
        {
            std::cout << "hello world!" << extraText << "\n";
        }
    }

private:
    std::string extraText;
};

int main()
{
    SafeRefTo<Actor> manager;
    {
        auto tracked(manager.makeTracked());
        manager.invoke(&Actor::act, 1); //output: hello world!
    }//"tracked" goes out of scope: the pointer in "manager" is now invalidated.
    manager.invoke(&Actor::act, 1);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.