Accepting the Destructor
A uC++ task can _Accept( ~Type ) in its main loop to let the caller who is destroying/stopping the object terminate it cleanly. Semantics differ from accepting a normal member: the caller is pushed onto the A/S stack instead of the acceptor; control resumes after the accept statement without running the destructor body. The task then cleans up and the destructor actually runs only when the caller is popped off.
Why different semantics from a normal accept?
Because storage can’t be freed while the task’s thread is still inside the object. Normal accept lets the acceptor continue after the call; for a destructor, the acceptor must finish cleanup first. Pushing the caller (not the acceptor) onto A/S stack turns the task into a monitor for its dying moments, thread halted, still safe to clean up condition queues, flush buffers, etc.
Idiom
_Task BoundedBuffer {
// ...
void main() {
// start up
for ( ;; ) {
_Accept( ~BoundedBuffer ) { // terminate ?
break;
} or _When ( count != 20 ) _Accept( insert ) { ... }
or _When ( count != 0 ) _Accept( remove ) { ... }
}
// close down — flush, reactivate blocked tasks, etc.
}
};
int main() {
BoundedBuffer buf;
// ... producers & consumers run, then terminate
// implicit delete buf at scope exit fires ~BoundedBuffer
}Key properties
- Caller blocks immediately on destructor call if task’s thread is still active.
- When destructor is accepted, caller pushed on A/S stack instead of acceptor.
- Control returns to the accept statement without executing the destructor body, break out of loop, clean up.
- Task now behaves like a monitor (thread halted) while close-down runs.
- Destructor body runs when the caller is eventually popped off A/S stack.
- Close-down can reactivate blocked tasks on condition queues or the A/S stack so they don’t deadlock.
Two idioms compared
Stop member, decouples termination from deallocation. Caller must remember to stop before delete.
_Task BoundedBuffer {
public:
void stop() {} // empty mutex member
private:
void main() {
// start up
for ( ;; ) {
_Accept( stop ) { // terminate ?
break;
} or _When( count != 20 ) _Accept( insert ) { ... }
or _When( count != 0 ) _Accept( remove ) { ... }
}
// close down
}
};
int main() {
BoundedBuffer buf;
// create / delete producer & consumer tasks
buf.stop(); // no outstanding calls to buf
// maybe print statistics using buf
} // implicit delete buf fires ~BoundedBufferAccept destructor, fuses the two. Right idiom when termination and deallocation always happen together:
void main() {
for ( ;; ) {
_Accept( ~BoundedBuffer ) { // accept destructor call
break;
} or _When( count != 20 ) _Accept( insert ) { ... }
or _When( count != 0 ) _Accept( remove ) { ... }
}
// close down
}
int main() {
BoundedBuffer buf;
// ... use it ...
} // implicit delete buf — destructor call is what main()'s _Accept picks upNo separate stop() call. Clean-up code runs before the destructor body; destructor body runs when the caller pops off the A/S stack.