Last time, we wrote a helper function for converting an awaitable into a winrt::
fire_
and_
forget
, as well as another helper function that takes a lambda that returns an awaitable, and which invokes the lambmda as a winrt::
fire_
and_
forget
.
After I wrote the two functions, I wondered if I could unify them. Mostly because I wanted to use the same name no_await
for both functions.
This took me down the horrible rabbit hole known as C++ template metaprogramming. I wanted two versions of the function, one that is used if the parameter is awaitable, and another that is used if the parameter is a functor. This led me to try using things like std::
enable_
if
to detect which case I’m in, and that led to lots of frustration, especially because there’s no easy way to detect if a type is awaitable. My closest approach was
template<typename T, typename Promise = std::void_t<>> struct is_awaitable : std::false_type {}; template<typename T> struct is_awaitable<T, std::void_t<typename std::experimental::coroutine_traits<T>::promise_type>> : std::true_type {}; template<typename T> inline constexpr bool is_awaitable_v = is_awaitable<T>::value;
which infers that a type is awaitable by sniffing whether it has an associated promise_
type
. This isn’t foolproof, because some types like winrt::
fire_
and_
forget
have a promise_
type
that cannot be awaited.
My first realization was that I could flip the test. Instead of checking whether the argument is awaitable, I check whether it is invokable.
My second realization was that I didn’t have to do fancy template metaprogramming at all. I could take advantage of the new if constexpr
feature.
template<typename T> fire_and_forget no_await(T t) { if constexpr (std::is_invocable_v<T>) { co_await t(); } else { co_await t; } }
Now you can use no_
await
with awaitables or functors that return awaitables.
void Stuff() { // Start this operation but don't wait for it to finish no_await(DoSomethingAsync()); // Start this sequence of things and don't wait for // them to finish. no_await([=]() -> IAsyncAction { co_await Step1Async(); // Step 2 doesn't start until Step 1 completes. co_await Step2Async(); }); }
On the other hand, for the case of the lambda passed to no_
await
, you could just declare your lambda as returning a winrt::
fire_
and_
forget
, and then you wouldn't need no_
await
.
void Stuff() { // Start this operation but don't wait for it to finish no_await(DoSomethingAsync()); // Start this sequence of things and don't wait for // them to finish. invoke_async_lambda([=]() -> winrt::fire_and_forget { co_await Step1Async(); // Step 2 doesn't start until Step 1 completes. co_await Step2Async(); }); }
But I like the fact that the first example uniformly uses the name no_
await
to describe the concept of "I'm not going to wait for this thing to finish." And also I'm perhaps unduly attached to the cute name.
As a by-product, you could also `no_await(DoSomethingAsync)`, which is somehow bad style.
Recently, I was implementing overloads of template<typename Writer> void f(Writer&& writer), and the overload resolution needed to depend on whether std::forward<Writer>(writer)(os) would be valid given std::ostream& os, or whether Writer would prefer some other type of parameter. I considered using std::is_invocable_v<Writer&&, std::ostream&> but decided not to, because that answers a different question: whether std::invoke(std::forward<Writer>(writer), os) is valid. The latter would also allow e.g. &std::ostream::flush as a writer and I didn't want to support that.
I’m conflicted on the naming. On the one hand, it’s cute. On the other hand, we’ve now got two vastly different behaviours (co_await vs no_await) that are only distinguished by one letter and the presence/absence of a pair of parenthesis. Something that could be easily overlooked in haste.