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_;
}
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;
}
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_
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;
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:
- A “delete detector” (in
perform()
’s stack frame) that detects when an object’s destructor has been called. - 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;
};
Both the
delete_signaler
copy constructor and assignment operator intentionally do nothing. Specifically,detector_
must not be copied if and when thedelete_signaler
is. If it were copied, then one of two bad things would happen:
- If the
delete_signaler
copy is destroyed before the callback returns, then the embeddeddelete_signaler
may invalidate the signal sent (or not sent) by thedelete_signaler
original.- If the
delete_signaler
copy is destroyed after the callback returns, thendetector_
will likely point to adelete_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;
}
}
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_;
};
Note that calling
set_success()
is needed only in cases likeoperation
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 callset_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;
};
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
}
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;
}
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;
}
}
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;
}