0

I've been making use of callbacks to reduce coupling between some C++ classes. To define terms: I'll call the class making the callbacks the caller, and the class receiving the callback the callee. Typically (but not necessarily), the callee will own the caller. By design, the caller has no knowledge of the callee.

I'm running into an issue relating to the lifespan of the caller object: It has no guarantee that it will still be alive after making any arbitrary callback. Take this basic example:

void caller::f()
{
    /* Some work */
    if (...)
    {
        /* [1] Execute callback */
        _callee->callback(this);
    }
    /* [2] Some more work */
}

Say that the callee has dynamically allocated the caller, and has registered for the callback specifically to wait for a certain condition to occur. When it does, the callee will delete the caller from within the callback at [1]. If that's the case, control will then return to caller::f, but this will have been deleted, and any code at [2] will more than likely crash.

In the general case, the caller can't assume anything about the callee. It doesn't know if the callee owns this, or if it might deallocate this, so I would need some general means of preventing deallocation for the scope of the caller's member function.

I believe a possible solution revolves around boost::shared_ptrs and enable_shared_from_this, though I've never used it. Since these callbacks are running very frequently (40+ times per second) on mobile devices with limited processing power, I'm also worried about the overhead of creating and passing out that many shared_ptrs.

Delegation is a pretty common pattern in Objective-C. I'm far less familiar with common C++ design patterns. Is there any quick-and-easy fix for this issue? If not, how would this design typically be accomplished in C++?

Matt Wilding
  • 20,115
  • 3
  • 67
  • 95
  • 1
    "Say that the callee has dynamically allocated the caller... the caller will delete the callee from within the callback...". That just doesn't sound like a good design at all. – Chad Apr 03 '12 at 15:43
  • @Chad: Why is that a bad design? If the owner no longer requires the services of the worker after receiving a callback, it needs to delete it. – Matt Wilding Apr 03 '12 at 15:50
  • @Chad: Sorry, I swapped "caller" and "callee" in the sentence in question. Yes, the way I wrote it would be woefully bad design. I should have picked terms with more that a single letter difference... – Matt Wilding Apr 03 '12 at 15:56

3 Answers3

1

When the callee deletes caller, caller's destructor is called. It is there that you should make sure f has finished.

I am guessing f is a thread, so easiest solution would be:

thread:

running = true;
while (!must_exit)
    /* do something */

destuctor:

thread->must_exit = true;
while (thread->running)
    sleep(a_little);
/* continue with destruction */

If f is not a thread, the same principle can apply where f makes its object (and through that its destructor) know when it is running and when not.


If you don't want to go with a destructor approach, you can still implement this functionality through a function that the callee calls, telling f to never run again and wait until it stops. Then the callee continues with deletion of caller.

So something like this:

void caller::f()
{
    if (being_deleted)
        return;
    running = true;
    /* Some work */
    if (...)
    {
        /* [1] Execute callback */
        _callee->callback(this);
    }
    /* [2] Some more work */
    running = false;
}

void caller::make_f_stop()
{
    being_deleted = true;
    while (running)
        sleep(a_little);
}
Shahbaz
  • 46,337
  • 19
  • 116
  • 182
  • I could add some specialized code in the destructor, but it would vary on a class-by-class basis. It seems simpler to just avoid the destructor being called in the first place. Also, `f` isn't a thread in this case, just some encapsulated business logic. – Matt Wilding Apr 03 '12 at 16:07
  • Sorry, I swapped "caller" and "callee" in a critical sentence in the question. The callee (owner) is actually deleting the caller (owned). – Matt Wilding Apr 03 '12 at 16:17
  • @MattWilding, I saw that. I updated the answer swapping callee and caller. Still, there is a part I added in the end you might want to see – Shahbaz Apr 03 '12 at 16:21
  • Thanks, sorry for the confusion. That'll do the trick. I'll just have to weigh the relative merits against `boost::enable_shared_from_this`. – Matt Wilding Apr 03 '12 at 16:25
1

Go ahead and use the shared pointer, though if possible use std::shared_ptr instead of boost::shared_ptr. It's in the (current) standard library, so no need to add an unnecessary boost dependency. If you're already using boost, then that's fine too.

You didn't specify what sort of mobile device you're talking about, but the processors in modern smartphones run at hundreds or thousands of megahertz, and even low-power phones often run Java programs (with garbage collection) just fine. Shared pointers are basically reference counted. It's not a resource-intensive activity.

If your device is able to actually run the callback more than 40 times per second, I doubt it will have any trouble with shared pointers. Don't prematurely optimize for execution speed. DO prematurely optimize for safety and sanity.

kenm
  • 23,127
  • 2
  • 43
  • 62
  • I would use `std::shared_ptr`, but due to dependencies on static libs compiled with the old standard library I can't. I'll use the shared_ptr approach, since it seems like the most elegant. I just wanted to make sure there wasn't a simpler approach before I boostify. – Matt Wilding Apr 03 '12 at 16:04
1

The usual way of bypassing code that you can't execute anymore is to throw an exception. This should be done by the callee at the point where it would normally return to the caller, after it has deleted the caller. The exception would be caught in the caller code at the end of the function.

I can't say I like this solution, but I think that stems from the unusual situation of the callee owning the caller.

I don't know how smart pointers would help since there's nobody to own the second copy of the pointer.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • In order for the smart pointer to work, I would make the constructor private and add a factory function that returns a smart pointer for creation. The caller actually doesn't own the callee. It's the other way around, though I did swap the words at one point in the question. – Matt Wilding Apr 03 '12 at 16:30
  • @MattWilding, I just made the same mistake - I knew the callee owned the caller, I just misworded it in my answer. Fixing now. As for the smart pointer, whoever owns the smart pointer owns the object unless there's a second copy around. And it's still not clear to me who would have the second copy, no matter how the first copy is created. – Mark Ransom Apr 03 '12 at 16:35
  • The second pointer would just be created internally in `f`. It's basically allowing the caller to take partial ownership of itself until the second shared pointer goes out of scope at the end of the function. – Matt Wilding Apr 03 '12 at 16:41
  • @MattWilding, it appears you have already solved your problem and you're just waiting for someone to say it's a good idea. Wait no more, it looks good to me. This might help: http://stackoverflow.com/questions/712279/what-is-the-usefulness-of-enable-shared-from-this – Mark Ransom Apr 03 '12 at 17:00
  • It's also interesting to me that you say it's "unusual" for the callee to own the caller. I'll normally have an Owner (callee) that will create a Worker (caller), and register for callbacks from the Worker. The Owner then deletes the Worker when it deems necessary. Threading is not necessarily involved, but it may be. This isn't a typical situation? – Matt Wilding Apr 03 '12 at 17:00
  • @MattWilding, perhaps the unusual part is to have the caller get deleted in the middle of a callback. – Mark Ransom Apr 03 '12 at 17:02
  • Yeah, I had a feeling that it would work, but I was kind of hoping there would be a common solution (that didn't involve a dependency on boost). C++ is new territory. Thanks. – Matt Wilding Apr 03 '12 at 17:05