Some follow-up notes on
Emulating the C# using
keyword in C++,
primarily for the benefit of
people from reddit who stumbled into the series and
didn’t understand the context of the discussion,
because this was really part 6 of the series begun the previous week,
even though it wasn’t labeled as such.
(And the title itself was a party trick rather than a serious
proposal.)
The main complication that prevented us from using RAII was the use of the Parallel Patterns Library (PPL) to express asynchronous programming in C++.¹ The most general pattern for asynchronous programming is that you start an operation, and then specify a callback to be invoked when the operation completes. In traditional C-style programming, this callback is a boring function pointer, coupled with some reference data so the callback has context for why it is being called back. C++ provides lambdas which let you express the continuation as a callback object, which is much more convenient since you can express the control flow inline instead of having tiny pieces of control flow scattered all over your program. And lambda capture makes it easy to express what pieces of information needs to be carried forward to the continuation.
If we didn’t have any asynchronous operations, then a basic RAII
class would do the trick:
When the RAII class destructs, the cleanup operation occurs.
(In our example, the cleanup operation is calling Close
.)
The difficulty in the asynchonous case
is that it is cumbersome to keep carrying
this RAII class forward and preventing it
from destructing until the entire chain of continuations has completed.
That’s where the ensure_
close
and
shared_
close
classes entered the picture.
But you still had to remember to carry them forward.
The magical step was the
introduction of the not-yet-standard-but-hopefully-soon
co_await
keyword.
This
transforms the function into a state machine,
where each co_await
represents
the end of a state.
The current execution state is saved, and execution of the task
suspends.
When the awaited operation completes, the execution state is
restored and the function resumes execution.
This transformation is very tedious and error-prone to perform
by hand (especially when there are loops and branches),
and in particular,
it preserves RAII semantics:
The automatic variables created by the pre-transformed function
become part of the execution state,
and they are destructed at the “natural” time they would have
been destructed prior to transformation.
As a result, switching to co_await
brings us full circle
back to plain old RAII.
Behind the scenes, the compiler is doing the wacky transformations
that we tried to mimic with ensure_
close
and shared_
close
.
But co_await
lets us write the code in a far more natural way.
¹
There’s also
std::future
,
but
its lack of composability
makes it a poor choice for asynchronous programming.
0 comments