Detecting Deletion

Paul J. Lucas - Aug 31 '18 - - Dev Community

Suppose you’re implementing a class that uses a callback:

class operation {
public:
    struct done_callback {
        virtual ~done_callback();
        virtual void operation_done( operation *op ) = 0;
    };

    operation() { }

    bool perform( done_callback *cb );

private:
    bool success_ = false;
};

bool operation::perform( done_callback *cb ) {
    // ...
    if ( cb != nullptr )
        cb->operation_done( this );
    return success_;
}
Enter fullscreen mode Exit fullscreen mode

Aside on naming callbacks

Even though a callback seems like a good candidate for a functor, that is:

virtual void operator()( operation *op ) = 0;

The problem with this is that it restricts a class from implementing multiple callbacks simultaneously. Hence, it’s better to give callbacks explicit names.

Further suppose we implement done_callback like:

struct my_callback : operation::done_callback {
    void operation_done( operation *op ) override;
};

void my_callback::operation_done( operation *op ) {
    // ...
    delete this;
    delete op;
}
Enter fullscreen mode Exit fullscreen mode

At the end of operation_done(), the callback deletes itself and the operation now that it’s done. The problem is that perform() does:

    return success_;                // really: this->success_
Enter fullscreen mode Exit fullscreen mode

and so may dump core because the operation was deleted by the callback. So the general question is: how can a member function tell whether a callback deleted its object?

Detecting Deletion

One way to detect deletion is by having the callback signature be either:

        virtual bool operation_done( operation *op ) = 0;
        virtual void operation_done( operation *op, bool *deleted ) = 0;
Enter fullscreen mode Exit fullscreen mode

The first would return true only if the callback deleted its caller; the second could set *deleted to true. While either would work, they feel kind of clunky and not general-purpose enough. Plus they require cooperation from the callback, hence aren’t surefire.

Ideally, we want to know if ~operation() was called. Perhaps have the destructor itself set a “destructor called” flag that perform() could check. But how? It obviously can’t use a data member of *this for the flag. But it can set a flag in perform()’s stack frame.

To do this, we need two things:

  1. A “delete detector” (in perform()’s stack frame) that detects when an object’s destructor has been called.
  2. A “delete signaler” (as a data member of operation) that runs when ~operation() has been called and signals the detector.

Here’s the declaration for delete_signaler:

class delete_detector;

class delete_signaler {
public:
    delete_signaler()  { }

    delete_signaler( delete_signaler const& ) {
        // intentionally do nothing
    }

    ~delete_signaler();

    delete_signaler& operator=( delete_signaler const& ) {
        // intentionally do nothing
        return *this;
    }

    void set_success( bool success );

private:
    delete_detector *detector_ = nullptr;
};
Enter fullscreen mode Exit fullscreen mode

Both the delete_signaler copy constructor and assignment operator intentionally do nothing. Specifically, detector_ must not be copied if and when the delete_signaler is. If it were copied, then one of two bad things would happen:

  1. If the delete_signaler copy is destroyed before the callback returns, then the embedded delete_signaler may invalidate the signal sent (or not sent) by the delete_signaler original.
  2. If the delete_signaler copy is destroyed after the callback returns, then detector_ will likely point to a delete_detector that’s since been destroyed.

Note that it’s insufficient simply not to declare either a copy constructor or assignment operator because the compiler will auto-generate both that will copy detector_. Hence, both must be declared explicitly to do nothing.

The body of set_success() is:

void delete_signaler::set_success( bool success ) {
    if ( detector_ != nullptr ) {
        assert( !detector_->destructor_called() );
        detector_->status_ = success ?
            delete_detector::DTOR_CALLED_SUCCESS :
            delete_detector::DTOR_CALLED_FAILURE;
    }
}
Enter fullscreen mode Exit fullscreen mode

To use a delete_signaler, one has to be declared as a data member of operation and call set_success() upon destruction:

class operation {
public:
    // ...
    ~operation() {
        delete_signaler_.set_success( success_ );
    }

private:
    bool success_;
    delete_signaler delete_signaler_;
};
Enter fullscreen mode Exit fullscreen mode

Note that calling set_success() is needed only in cases like operation where there is a “success” result. If no such result is needed, but the caller still needs to detect whether the callback has deleted it, then there is no need to call set_success().

All the work of signaling the detector is done in ~delete_signaler() (more later).

Here’s the declaration for delete_detector:

class delete_detector {
public:
    explicit delete_detector( delete_signaler &signaler );
    ~delete_detector();

    bool destructor_called() const {
        return status_ != DTOR_NOT_CALLED;
    }

    bool success() const {
        return status_ == DTOR_CALLED_SUCCESS;
    }

private:
    enum status {
        DTOR_NOT_CALLED,
        DTOR_CALLED_VOID,
        DTOR_CALLED_SUCCESS,
        DTOR_CALLED_FAILURE
    };

    delete_signaler &signaler_;
    status status_ = DTOR_NOT_CALLED;

    delete_detector( delete_detector const& ) = delete;
    delete_detector& operator=( delete_detector const& ) = delete;

    friend class delete_signaler;
};
Enter fullscreen mode Exit fullscreen mode

A delete_detector would be used like:

bool operation::perform( done_callback *cb ) {
    // ...
    if ( cb != nullptr ) {
        delete_detector detector{ delete_signaler_ };
        cb->operation_done( this );
        if ( detector.destructor_called() )
            return detector.success();
    }
    return success_;                // this->success_ is safe to access
}
Enter fullscreen mode Exit fullscreen mode

The constructor for delete_detector is fairly straightforward in that it simply sets the signaler’s detector_ to this:

delete_detector::delete_detector( delete_signaler &signaler ) :
    signaler_{ signaler }
{
    assert( signaler_.detector_ == nullptr );
    signaler_.detector_ = this;
}
Enter fullscreen mode Exit fullscreen mode

The destructor for delete_detector not surprisingly does the reverse by setting the signaler’s detector back to nullptr — but only if the signaler’s destructor was not called:

delete_detector::~delete_detector() {
    if ( !destructor_called() ) {
        assert( signaler_.detector_ == this );
        signaler_.detector_ = nullptr;
    }
}
Enter fullscreen mode Exit fullscreen mode

One last bit of code is the destructor for delete_signaler that sets the detector’s status_ to DTOR_CALLED_VOID but only if it’s DTOR_NOT_CALLED, i.e., set_success() was not called:

delete_signaler::~delete_signaler() {
    if ( !detector->destructor_called() )
        detector->status_ = delete_detector::DTOR_CALLED_VOID;
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .