Destructor callbacks and mcguffins

It is possible to make arrangements to have a closure, or some callable object, a.k.a. a callback get invoked when a specific x::obj or its subclass, a.k.a. a mcguffin, gets destroyed when the last x::ref or an x::ptr to the mcguffin goes out of scope. This object then becomes 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 a reference-counted object whose methods and members may not be necessarily important, but 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 code a separate notification 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. With typical situations there will be other ways for things to go off the beaten path, too.

A mcguffin-oriented approach is simpler. The message to a thread is some kind of an object, which serves double-duty as a mcguffin. 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 a mcguffin because its destructor callbacks get 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 was destroyed.

#include <x/ref.H>
#include <x/obj.H>
#include <x/ondestroy.H>

class mcguffinObj : virtual public x::obj {

// ...

};

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

// ...
myobj->ondestroy([]
   {

// ...

   });

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::ptr documentation for more information on destructor callbacks.

Note

Destructor callbacks should not throw exceptions, because it essentially becomes a part of the mcguffin's destructor, and cannot throw any exceptions. All exceptions must be caught.

A basic destructor callback

x::destroy_callback is a simple elementation of a common design pattern where some mcguffin's destruction does not itself takes any immediate action. Rather, at some point later, a check is made whether the object is already destroyed. If not, wait for its destruction.

#include <x/destroy_callback.H>

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

// ...

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

mcguffin->ondestroy([cb] { cb->destroyed(); });

// ...

cb->wait();

x::destroy_callback implements wait() which does not return until its destroyed() gets invoked, presumably by another thread. wait() returns immediately if destroyed() was already invoked.

Note

x::destroy_callback is designed for simple one-off situations involving a single mcguffin. It's your responsibility to attach it as a destructor callback to at least one object, before invoking wait(). If it's not been ondestroy()ed anywhere, its destroyed() never gets invoked, and wait() waits forever.

A destructor callback guard

A x::destroy_callback::base::guard is an object meant to be instantiated in automatic scope or as a class member. Its destructor waits until the objects placed in its care get destroyed, before the destructor itself returns:

void run()
{
    x::destroy_callback::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 really 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::destroy_callback::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.

Note

x::destroy_callback::base::guard does not implement copy contructors or assignment operators. It should always be instantiated either in automatic scope, or as a class member.

x::destroy_callback::base::guard_object is a convenient template to instantiate a guard for a single x::ref.

typedef x::ref<widgetObj> widget;

class containerObj : virtual public x::obj {

    x::destroy_callback::base::guard_object<widget> title;

// ...
};

// ...
containerObj::containerObj() : title(widget::create())
{
// ...

   title->method();
}

x::destroy_callback::base::guard_object's template parameter must be an x::ref. x::destroy_callback::base::guard_object declares a subclass of this x::ref, that's privately guarded. When x::destroy_callback::base::guard_object goes out of scope and gets destroyed, if there are any other x::refs or x::ptrs to the same object, x::destroy_callback::base::guard_object waits until all other x::refs and x::ptrs go out of scope and gets destroyed. x::destroy_callback::base::guard_object's destructor does not return until the referenced object's destructor returns.

x::destroy_callback::base::guard_object inherits *() and ->() operators from the underlying x::ref, so that it can be used just like one. Although the guard object itself is not copy-constructible or assignable, the x::ref can be copied and passed around (with all copies required to be destroyed, before the guard's destructor returns).

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.

Instead of invoking some mcguffin's ondestroy() method to attach a destructor callback to the mcguffin, construct an x::ondestroy class instance:

#include <x/ondestroy.H>

class mcguffinObj : virtual public x::obj {

// ...

};

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

auto ondestroy_hook=x::ondestroy::create([]
                  {
                        // ...
                  }, mcguffin, true);

// ...

ondestroy_hook->cancel();

The destructor callback lambda stays attached to its mcguffin until x::ondestroy's cancel() gets invoked. If the mcguffin is not destroyed by the time cancel() gets invoked, the destructor callback does not get invoked as part of the mcguffin's eventual destruction. 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>

// ...

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

x::on_any_destroyed([]
                   {
                      // Something was destroyed
                   }, objects.begin(), objects.end());

x::on_any_destroyed() invokes a lambda when any one of several objects gets destroyed. Its parameters are a lambda to invoke, and a a beginning and an ending input iterator for a sequence of references to reference-counted object class instances.

x::on_any_destroyed() results in the lambda getting invoked when any of the objects in the sequence go out of scope and gets destroyed. The lambda callback 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 the lambda getting invoked on their behalf.

The input sequence is expected to produce strong x::refs only. However, it's an arbitrary input sequence. Once an 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 the lambda gets called even before x::on_any_destroyed() returns.

The lambda also gets called, prior to x::on_any_destroyed() returning in the event that the iterators define an empty input sequence. x::on_any_destroyed() works by attaching an x::ondestroy() callback to all of the objects in the sequence to a mcguffin that invokes the lambda, with the first x::ondestroy() call releasing the mcguffin, whose destructor invokes the lambda. If the iterator sequence is empty, the mcguffin goes out of scope immediately, prior to x::on_any_destroyed() returning.