DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on • Edited on

Detecting Deletion

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

Top comments (0)