Let's devise a C++ implementation, using POSIX threads. The worker class would inherit from the base class RefTarget that would embody the functionality we're interested in. The class WeakReaf would implement a weak reference. Listing One is a reasonable class interface. The templates may be used to control the pointer types better but it's easier to show the example without them, with simple inheritance.
class RefTarget { friend class WeakRef; public: RefTarget(void *owner); // this pointer will be held in the weak references ~RefTarget(); WeakRef *newWeakRef(); // create a new weak reference protected: void invalidate(); // called from the subclass destructor void freeWeakRef(WeakRef *wr); // notification from a WeakRef that it gets destroyed private: // prevent the default and copy constructors and assignments RefTarget(); RefTarget(const RefTarget&); void operator=(const RefTarget&); }; class WeakRef { friend class RefTarget; public: ~WeakRef(); WeakRef *copy(); // create a copy of this reference void *grab(); // make the reference strong, return its current value void release(); // release the reference back to weak protected: WeakRef(RefTarget *target); // called from copy() and from RefTarget bool invalidate1(); // called from RefTarget invalidation void invalidate2(); // called if invalidate1() returned true private: // prevent the default and copy constructors and assignments // (they may be added but the code is simpler without them) WeakRef(); WeakRef(const WeakRef&); void operator=(const WeakRef&); };
Listing Two shows how you use it. The listing includes only the parts related to the weak references, skipping most of the thread control and such. Note that the Worker invalidates its RefTarget at the start of the destructor. This must be done before the object starts being taken apart. Leaving things until the RefTarget destructor would be too late, since the superclass destructor gets called only after the subclass destructor. Invalidation insures of two things: that the new weak references stop being given out (or are given out as already invalid) and that all the existing weak references get invalidated. The weak references are invalidated in two steps, by the reasons that I discussed later.
class Worker : public RefTarget { public: Worker() : RefTarget(this) { ... } ~Worker() { RefTarget::invalidate(); .... } ... }; class Listener { public: Listener(WeakRef *ref) : ref_(ref), done_(false) { ... } ~Listener() { delete ref_; ... } execute() { ... while(!done_) { ... accept connection ... Client * cl = new Client(connection, ref_.copy()); ... cl.start(); // starts the thread with execute() method } ... } protected: WeakRef *ref_; bool done_; }; class Client { public: Client(Connection *conn, WeakRef *ref) : conn_(conn), ref_(ref) { ... } ~Client() { delete ref_; ... } execute() { while(!conn_.eof()) { ... accept request ... error = 0; Worker *w = (Worker *)ref_->grab(); if (w == 0) error = WORKER_GONE; else error = w->call(...); ref_->release(); ... return result ... } ... } protected: Connection *conn_; WeakRef *ref_; }; // hypothetical main() ... Worker *w = new Worker; Listener l = new Listener(w->newWeakRef()); w->start(); l->start(); ...
The WeakRef constructor is protected. It's never ever called directly. Instead all the creation happens either through the factory method newWeakRef() of the target object, or through the copy() method. This makes sure that nobody will create a reference to an object that is being or has been destroyed.
The WeakRef methods grab() and release() are always called in pairs, it makes both the usage and the implementation simpler. If the reference became invalid, grab() would return a NULL pointer. This should always be checked before performing any actions. But if it's not NULL then the worker object is guaranteed to stay put until release() is called.
An important assumption in this code is that one weak reference is used by one thread only. This thread must take care to always release the reference after grabbing it, and to release the reference before destroying it. If the reference needs to be passed to another thread, it should be copied first, and then the copy given to that other thread. Of course, other means may be used to implement a consistent access from multiple threads to the same reference, but making extra copies is easier.