In general, the explicit support for move-only handlers ties in with allocation order guarantees that Asio makes:
As noted previously, all resources must be released prior to calling the completion handler.
This enables memory to be recycled for subsequent asynchronous operations within an agent. This allows applications with long-lived asynchronous agents to have no hot-path memory allocations, even though the user code is unaware of associated allocators.
Specifically, the second example
auto op = async_write_messages(socket, "Testing deferred\r\n", 5, asio::deferred);
defines a packaged operation op
with type
asio::deferred_async_operation<
void(boost::system::error_code),
asio::detail::initiate_composed_op<void(boost::system::error_code),
void(asio::any_io_executor)>,
async_write_messages_implementation>
Looking at the implementation thereof, indeed we see exactly what you surmised:
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(Signature) CompletionToken>
auto operator()(
BOOST_ASIO_MOVE_ARG(CompletionToken) token) BOOST_ASIO_RVALUE_REF_QUAL;
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(Signature) CompletionToken>
auto operator()(
BOOST_ASIO_MOVE_ARG(CompletionToken) token) const &;
Which, removing macro noise, in c++20 becomes:
template <asio::completion_token_for<Signature> CompletionToken>
auto operator()(CompletionToken&& token) &&;
template <asio::completion_token_for<Signature> CompletionToken>
auto operator()(CompletionToken&& token) const&;
The &&
-qualified overload optimizes execution. This makes intuitive sense when you realize that a deferred handler may merely represent deferred_values
, to be passed to the user-handler. These - effectively callback arguments - may also be expensive to copy, or move-only.
In this case, deferred_async_operation
implements a function object that delays initiation of another asynchronous operation. The initiation function takes arguments that might, again, be expensive to copy, or be move-only.
Indeed the rvalue-ref-qualified version supports those move-semantics, where the const-qualified version does not (again Asio code heavily redacted for legibility, and assuming C++14 or up):
template <typename CompletionToken, std::size_t... I>
auto invoke_helper(CompletionToken&& token, std::index_sequence<I...>)
{
return asio::async_initiate<CompletionToken, Signature>(
std::move(initiation_), token, std::get<I>(std::move(init_args_))...);
}
template <typename CompletionToken, std::size_t... I>
auto const_invoke_helper(CompletionToken&& token, std::index_sequence<I...>) const&
{
return asio::async_initiate<CompletionToken, Signature>( //
initiation_t(initiation_), token, std::get<I>(init_args_)...);
}
Does It Matter?
Arguably in code that doesn't support ref-qualification, the unqualified version of operator()
would successfully pass lvalue-references to the initiation function. This would even work for move-only types IFF the arguments were taken by lvalue-reference. If mutable, those could even be moved from.
The more precise forwarding allows initiation where move-only ("sink") parameters are taken by value, too.
In cases where copies are avoided, this has the important benefit of optimizing the (de)allocation patterns of the application. Consider what would happen if one of the arguments involved contains a reference-counted resource (e.g. shared_ptr
). Even if the wrapping type (e.g. deferred_async_operation
) goes away "immediately" after invocation, there's can be a difference in the order of allocation/de-allocations when the ref-counted resource temporarily has a non-unique refcount.
TL;DR
I'd like to boil it down to expressive code: callables that are meant to be invoked only once should express that "unique invocation" much like any other move-only type signal "unique ownership" by the presence of std::move()
.
There are places where it matters, and a general-purpose library like Asio should not impose unnecessary overhead.
Illustrating - Live Demos
In the example, the async_write_messages_implementation
is move-only because it contains unique_ptr
members. Therefore, going through the const_invoke_helper
would not be able to compile: https://godbolt.org/z/KTc5ooPhT
You can fix it by changing the unique_ptr
to shared_ptr
, but now you run into the issue described where ownership of the resources will not be unique during the call to async_write_messages_implementation::operator()
which leads to the reset()
s not releasing their resource until later: https://godbolt.org/z/n9fbE3zxh