Last time, we looked at a way to use the RAII design pattern (Resource Acquisition Is Initialization) to ensure that a function eventually got called on all code paths, even if one of the “eventually” involves waiting for a callback. We solved the problem by using an RAII type which performs the cleanup action in its destructor and moving that type into the capture of the callback lambda.
This plan requires that the function that accepts the callback lambda never tries to copy the lambda, because our RAII type is not copyable. (If it were copyable, the one-time action will get executed twice, once when the original and destructs, and once when the copy destructs.) But what if the function requires a copyable lambda?
void MySpecialFeature::OnButtonClick()
{
auto ensureDismiss = wil::scope_exit([self = shared_from_this()]
{ self->DismissUI(); });
try {
auto folder = PickOutputFolder();
if (!folder) {
return;
}
if (ConfirmAction()) {
if (m_useAlgorithm1) {
// StartAlgorithm1 invokes the lambda when finished.
StartAlgorithm1(file,
[ensureDismiss = std::move(ensureDismiss)] { });
} else {
RunAlgorithm2(file);
}
}
} catch (...) {
}
}
Suppose that you get an error somewhere inside the call to StartAlgorithm1 because it tries to copy the non-copyable lambda. How can you make the lambda copyable while still getting the desired behavior of cleaning up exactly once, namely when the lambda is run?
Start by wrapping the RAII type inside a shared_ptr:
// StartAlgorithm1 invokes the lambda when finished.
StartAlgorithm1(file, [ensureDismiss =
std::make_shared<decltype(ensureDismiss)>(move(ensureDismiss))]
{ ⟦ ... ⟧ });
There’s a bit of repetitiveness because shared_ does not infer the wrapped type if you are asking to make a shared_ by move-constructing from an existing object, so we have to write out the decltype(ensureDismiss).
Inside the body, we reset the RAII type, which calls the inner callable. Since all of the copies of the lambda share the same RAII object, the reset() call performs the cleanup operation on behalf of everybody.
// StartAlgorithm1 invokes the lambda when finished.
StartAlgorithm1(file, [ensureDismiss =
std::make_shared<decltype(ensureDismiss)>(move(ensureDismiss))]
{ ensureDismiss->reset(); });
In the weird case that all of the copies of the lambda are destructed without any of them ever being called, then when the final one destructs, it will destruct the RAII object, which will run the cleanup operation if it hasn’t been done yet.
The original problem to solve was that it was easy to forget a call to the cleanup method. I wonder if all this effort pays off, or if just a code review caught the problem and the missing line of code was added and that’s it. I mean, I don’t see the point of the whole new level of complexity added.
Or maybe this is a programming exercise, a “what if…”, in that case, then okay.
This function has four exit paths, all of which have to remember to clean up. When somebody adds another branch to the function, they will have to remember to add the cleanup code, and if the function is large or complex, this is easily overlooked. Just use an RAII type so that the cleanup happens automatically – the obvious code is the correct code. Isn’t that the whole point of RAII?