Destructor callbacks and mcguffins

It is possible to set up one or more callbacks that get invoked when some subclass of x::obj, some mcguffin, gets destroyed after the last x::ref or an x::ptr to the mcguffin goes out of scope. This callback is some method in an instance of a different, otherwise unrelated, class. The method gets invoked whenever the first class gets destroyed. Here, the first class is called a mcguffin. This name comes from the film director Alfred Hitchcock who used it to describe some minor prop or gadget that is of little inherent value, but is responsible for driving the major plot of a movie. (Although macguffin is the more popular spelling, I like the mcguffin spelling better, and I'm the one writing this documentation).

A mcguffin is some reference-counted object whose own implicit existence has some inherent purpose, and defines some part of its functionality, loosely speaking. Consider a typical situation of a thread sending some kind of a message to another thread. The first thread wants to know when the second thread processed this message.

You can write and implement an acknowledgement mechanism of some kind, but this quickly gets complicated. What if, for example, the second thread ended normally, but before it had a chance to process the message? You'll have to handle that eventuality too. In most non-trivial situations there will be other ways for things to go off the rails.

Using a mcguffin makes this much easier. When the message gets received by the thread, processed, and discarded, the last reference to the message object goes out of scope and the mcguffin gets destroyed. If the thread stops, presumably its message queue, together with all of its messages, also gets destroyed. In all cases, the message object gets destroyed. The message object is now a mcguffin, so its destructor callback gets invoked in all cases.

A mcguffin may be a very distinguished class on its own merits. It might have many methods and members of its own; but it's a mcguffin because it exists, and for no other reason.

A mcguffin can have more than one destructor callback. All destructor callbacks get invoked, one at a time, when the last x::ref or an x::ptr to the mcguffin, a reference-counted object goes out of scope and the object gets destroyed. The destructor callbacks get invoked immediately after the object's destructor returns and the object gets destroyed. Once invoked, each destructor callback does whatever it wants to do. The callbacks get invoked by whichever thread destroyed the last reference to the object, not necessarily the same thread that created it!

By the time the destructor callbacks get called, the mcguffin is already destroyed, and the destructor callback has no means to access it. But that's fine, the object's existence was what made it a mcguffin, and now the destructor callbacks get invoked because the object got destroyed.

#include <x/destroycallback.H>

class myDestructorCallbackObj : public x::destroyCallbackObj {

public:
      myDestructorCallbackObj();
      ~myDestructorCallbackObj() noexcept;
      void destroyed() noexcept override;
};

class mcguffinObj : virtual public x::obj {

// ...

};

typedef x::ref<myDestructorCallbackObj> myDestructorCallback;

myDestructorCallback mycb(myDestructorCallback::create());

x::ref<mcguffinObj> myobj=x::ref<mcguffinObj>::create();

myobj->addOnDestroy(mycb);

Subclassing x::destroyCallbackObj, defining the destroyed() method, and attaching the subclass instance to a mcguffin object results in destroyed() getting invoked when the last x::ref or an x::ptr to the mcguffin goes out of scope and the mcguffin gets destroyed.

The above example attaches a destructor callback to an instance of a mcguffinObj. This class may have its own methods, or serve some particular purpose that's unrelated to its role as a mcguffin. The above example leaves its methods unspecified. They don't matter. It's a mcguffin.

For a pure mcguffin, the destructor callback gets attached to a plain x::ref<x::obj>. An x::obj is nothing more than the base class for reference-counted objects, and a mere reference to this object has no other functionality, except serving as a convenient mcguffin, for any occasion.

See the x::ref documentation for more information on destructor callbacks.

Note

destroyed() is explicitly noexcept because it essentially becomes a part of the mcguffin's destructor, and cannot throw any exceptions. All exceptions must be caught.

Using a functor as a destructor callback

A helper create() template function instantiates a subclass of x::destroyCallbackObj whose destroyed() invokes a functor:

#include <x/destroycallback.H>

class mcguffinObj;

// ...

x::ref<mcguffinObj> myobj=x::ref<mcguffinObj>::create();

// ...
myobj->addOnDestroy(x::destroyCallback::create([]
        {
           // Functor goes here..
        });

In this example, create() instantiates a x::ref to a subclass of x::destroyCallbackObj whose destroyed() invokes the functor.

Additionally, the destroyed() functor wrapper takes care of catching and logging any exceptions thrown from the functor.

A basic destructor callback

x::destroyCallbackFlag is a reference to a simple subclass of x::destroyCallbackObj that implements the ability to wait for another object to be destroyed, presumably by some other thread, before proceeding:

x::destroyCallbackFlag cb=x::destroyCallbackFlag::create();

// ...

x::ref<x::obj> mcguffin;

mcguffin->addOnDestroy(cb);

// ...

cb->wait();

This subclass implements a wait(), that waits indefinitely for the mcguffin to get destroyed, if it's not already has been destroyed.

Note

x::destroyCallbackFlag is one time use only, and can be attached as a callback to only one object. It's your responsibility to attach it as a destructor callback to at least one object, before invoking wait(). If it's not been addOnDestroy()ed anywhere, its destroyed() can never get invoked, and wait() waits forever.

A destructor callback guard

A x::destroyCallbackFlag::base::guard is an object meant to be instantiated on the stack. Its destructor waits until other objects placed in its care get destroyed, before the destructor itself returns:

void run()
{
    x::destroyCallbackFlag::base::guard guard;

    x::ref<x::obj> mcguffin;

    guard(mcguffin);

// ...
}

When run()'s scope exits, its auto-scoped objects get destroyed in reverse order of declaration. But even after this mcguffin gets destroyed, other references to the mcguffin object might exist somewhere else, and/or used by other threads.

Use this guard object when you want to wait for these mcguffins to go out of scope and get destroyed. The guard object's destructor waits indefinitely until the guarded objects gets destroyed, if they're not destroyed already.

More than one object can be guarded. The guard's operator() attaches a destructor callback to the mcguffin, and the guard's own destructor waits until all attached destructor callbacks get invoked, if they're not already invoked.

The guard's operator() is a template function that returns the same x::ref that was passed to it. That makes it possible to do something like this:

void run()
{
    x::destroyCallbackFlag::base::guard guard;

    start_timer(guard(x::ref<x::obj>::create()));

// ...
}

start_timer() is some external function that takes a mcguffin as an argument. The mcguffin is no longer needed after invoking it, and run() has no further use for it; and no more references to the mcguffin should remain, start_timer() owns it entirely. Here, guard's operator() does its job, returns whatever x::ref it got, which gets subsequently passed to start_timer().

Subsequently, when guard goes out of scope, it waits for the mcguffin to get destroyed, wherever it is.

Revocable destructor callbacks

Attaching an ordinary destructor callback to a mcguffin is an irrevocable act. The destructor callback gets attached, and it gets invoked whenever the mcguffin ends up getting destroyed. Use x::ondestroy to set up destructor callbacks that can be revoked:

class myDestructorCallbackObj : public x::destroyCallbackObj {

public:
      myDestructorCallbackObj() noexcept;
      ~myDestructorCallbackObj() noexcept;
      void destroyed() noexcept;
};

class mcguffinObj : virtual public x::obj {

// ...

};

typedef x::ref<myDestructorCallbackObj> myDestructorCallback;

myDestructorCallback mycb(myDestructorCallback::create());

x::ref<mcguffinObj> mcguffin=x::ref<mcguffinObj>::create();

x::ondestroy ondestroyHook(x::ondestroy::create(myDestructorCallback, mcguffin,
                                           true);

// ...

ondestroyHook->cancel();

The destructor callback stays attached to its mcguffin until x::ondestroy's cancel() gets invoked. After the cancel(), the destructor callback will no longer get invoked when the mcguffin gets destroyed. If the mcguffin is already destroyed, and its destructor callbacks were already invoked, cancel() has no effect.

x::ondestroy itself is a reference to a reference-counted object, that can be freely passed around or copied. If the last parameter to x::ondestroy::create is true, cancel() gets automatically invoked when the x::ondestroy-referenced instance itself goes out of scope and gets destroyed. With false, when x::ondestroy gets destroyed the destructor callback becomes permanent.

If another thread is already running a just-destroyed object's callbacks, invoking cancel() explicitly blocks until the object's callbacks return. An explicit cancel()'s contract is that the destructor callback's destroyed() can never get called after cancel() returns. A cancel() that's automatically invoked, when x::ondestroy goes out of scope and gets destroyed, does not block if another thread is executing the object's destructors.

This is to allow an x::ondestroy to go out of scope and get destroyed as a direct or an indirect result of the same object's destructor callback getting invoked. Otherwise, this ends up deadlocking the thread with itself: something is executing the object's destructor callbacks, and x::ondestroy's destructor needs to wait for the callback to return, but that can't happen until the destructor itself returns.

Invoking a destructor callback when any of several objects gets destroyed

#include <x/ondestroy.H>

class myDestructorCallbackObj : public x::destroyCallbackObj {

public:
      myDestructorCallbackObj() noexcept;
      ~myDestructorCallbackObj() noexcept;
      void destroyed() noexcept;
};

typedef x::ref<myDestructorCallbackObj> myDestructorCallback;

myDestructorCallback mycb(myDestructorCallback::create());

// ...

std::list<x::ref<x::obj> > objects;

mycb->onAnyDestroyed(objects.begin(), objects.end());

onAnyDestroyed() is a template method in x::destroyCallbackObj. Its parameters are a beginning and an ending input iterator for a sequence of references to reference-counted object class instances.

onAnyDestroyed() results in the destructor callback's destroyed() getting invoked when any of the objects in the sequence go out of scope and gets destroyed. destroyed() gets invoked only once; for all practical matters, for the first object in the sequence that goes out of scope and gets destroyed. Subsequent destruction of other objects in the sequence does not result in destroyed() getting invoked on their behalf.

Of course, the same destructor callback can be attached to other mcguffins, whose own destruction will result in destroyed() invocations on their behalf.

The input sequence is expected to produce strong x::refs only. However, it's an arbitrary input sequence. Once a x::ref has been iterated over, it is no longer in scope, and, with a custom input iterator, its possible that it can get destroyed. If so, it's possible that destroyed() gets called before onAnyDestroyed() returns.

destroyed() also gets called in the event that the iterators define an empty input sequence.

Note

It's the mcguffin's addOnDestroy() method that gets invoked, and it takes the callback as a parameter. It is a destructor callback's onAnyDestroyed() method that gets invoked, and it takes an input sequence of mcguffins.

Note

onAnyDestroyed() gets implemented by x::destroyCallbackObj. The object referenced by x::destroyCallbackFlag is a subclass of x::destroyCallbackObj. x::destroyCallbackFlag works with onAnyDestroyed():

x::destroyCallbackFlag cb=x::destroyCallbackFlag::create();

cb->onAnyDestroyed(objects.begin(), objects.end());

All other semantics of x::destroyCallbackFlag remain in place.