Implementing Custom ASIO Completion Tokens: Extending Beyond use_future and use_awaitable
You can create custom ASIO completion tokens by defining a token type that implements operator()(CompletionHandler) and specializing the async_result trait to map the token and operation signature to a concrete handler type.
ASIO's extensibility hinges on the completion token abstraction, which decouples asynchronous operation initiation from result delivery semantics. While the library ships with built-in tokens such as use_future and use_awaitable, implementing custom ASIO completion tokens allows integration with callbacks, custom futures, or proprietary async frameworks. This guide examines the exact pattern used in the chriskohlhoff/asio repository to extend the library's async model.
How ASIO Discovers Completion Tokens
When you invoke an async operation like socket.async_read_some(buf, token), the library transforms your token into a concrete handler through the async_result trait defined in include/asio/async_result.hpp. The resolution process follows three distinct steps:
- Deduce the handler type using
typename asio::async_result<decay_t<CompletionToken>, Signature>::completion_handler_type - Construct the handler by invoking the token's
operator()(CompletionHandler), allowing the token to adapt or wrap the handler - Execute the initiation function with the constructed handler, which eventually gets invoked when the operation completes
The default specializations for built-in tokens reside in the implementation directory. For example, include/asio/impl/use_future.hpp defines async_result<use_future_t<Allocator>, R(Args...)> and its associated promise_handler, while include/asio/impl/use_awaitable.hpp handles coroutine-based tokens. These files serve as the canonical reference for custom implementations.
Building a Custom Token Structure
A production-ready custom token requires two coordinated components: the public token type and the async_result specialization.
The Token Type
The token itself is a lightweight struct that stores user-defined state and acts as a callable factory for the concrete handler. It must implement a templated operator() that accepts a completion handler and returns the adapted handler object.
The async_result Specialization
You must specialize async_result<YourTokenType, Signature> to tell ASIO how to:
- Determine the
completion_handler_type(the return type of your token'soperator()) - Retrieve the operation's final result through the
get()method
Specializations typically live in include/asio/impl/ following the library's convention, or adjacent to your token definition in include/asio/async_result.hpp.
Complete Implementation Example
Below is a minimal working implementation of a use_callback token that forwards operation results to a std::function<void(error_code, std::size_t)>.
Defining the Token
Create include/asio/use_callback.hpp with the token definition:
struct use_callback_t
{
explicit use_callback_t(std::function<void(asio::error_code, std::size_t)> cb)
: cb_(std::move(cb)) {}
template <typename CompletionHandler>
auto operator()(CompletionHandler&& h) const
{
struct wrapper
{
CompletionHandler inner;
std::function<void(asio::error_code, std::size_t)> cb;
void operator()(asio::error_code ec, std::size_t n) const
{
inner(ec, n);
cb(ec, n);
}
};
return wrapper{ std::forward<CompletionHandler>(h), cb_ };
}
std::function<void(asio::error_code, std::size_t)> cb_;
};
inline constexpr use_callback_t use_callback{ nullptr };
Specializing async_result
Create include/asio/impl/use_callback.hpp with the trait specialization:
#include <asio/async_result.hpp>
#include <asio/use_callback.hpp>
namespace asio {
template <typename Signature>
class async_result<use_callback_t, Signature>
{
public:
using completion_handler_type = decltype(
std::declval<use_callback_t>()(std::declval<std::function<void()>>())
);
void get() const noexcept {}
};
} // namespace asio
Usage in Practice
Invoke the custom token exactly like built-in alternatives:
#include <asio.hpp>
#include <asio/use_callback.hpp>
void echo_server(asio::ip::tcp::acceptor& acceptor)
{
acceptor.async_accept(
[&](asio::error_code ec, asio::ip::tcp::socket sock)
{
if (!ec)
{
std::array<char, 1024> buf;
sock.async_read_some(
asio::buffer(buf),
asio::use_callback(
[](asio::error_code ec, std::size_t n)
{
std::cout << "Read " << n << " bytes\n";
}));
}
});
}
Advanced Token Patterns
The ASIO repository demonstrates several sophisticated patterns you can adapt for custom tokens.
Adapter Tokens
Adapter tokens wrap and transform other tokens rather than creating handlers directly. The redirect_error token in include/asio/redirect_error.hpp exemplifies this pattern: it inherits from detail::partial_redirect_error, intercepts the completion signature to strip error codes, and forwards the modified handler to the underlying token. This approach lets you compose token behavior without rewriting handler logic.
Executor-Bound Tokens
Tokens requiring specific execution contexts, such as use_awaitable, expose a nested executor_type and a rebind member that returns a token bound to a different executor. Examine include/asio/impl/use_awaitable.hpp to see how the token stores an executor reference and constructs awaitable<R> objects that resume on the correct execution context.
Custom Allocators
The use_future implementation in include/asio/impl/use_future.hpp supports custom allocators through use_future_t<Allocator>. The allocator is stored within the token and forwarded to the handler's constructor via the rebind method. For memory-sensitive applications, follow this pattern by templating your token on an allocator type and propagating it during handler construction.
Key Source Files in the Repository
When implementing custom ASIO completion tokens, reference these authoritative implementations in the chriskohlhoff/asio repository:
include/asio/async_result.hpp— Core trait that drives token resolution and handler type deductioninclude/asio/impl/use_future.hpp— Full-featured token specialization demonstrating allocator support and promise-based resultsinclude/asio/impl/use_awaitable.hpp— Executor-bound token creating coroutine awaitablesinclude/asio/redirect_error.hpp— Adapter token pattern for modifying completion signaturesinclude/asio/experimental/use_promise.hpp— Alternative promise/future pair implementation showing advanced memory handling
Summary
- Custom ASIO completion tokens require a token type implementing
operator()(CompletionHandler)and anasync_resultspecialization definingcompletion_handler_typeandget(). - The token acts as a factory that wraps or adapts the library's internal handler before the operation initiates.
- Reference implementations in
include/asio/impl/use_future.hppandinclude/asio/impl/use_awaitable.hppprovide production-ready patterns for handler construction. - Advanced patterns include adapter tokens (composing behavior), executor-bound tokens (controlling execution context), and allocator-aware tokens (custom memory management).
- Place token definitions in
include/asio/and specializations ininclude/asio/impl/to maintain consistency with the library's architecture.
Frequently Asked Questions
What is the minimum requirement for a custom completion token?
A valid custom token must provide a callable operator() that accepts a completion handler and returns a handler-compatible object, plus a specialization of async_result<Token, Signature> that defines completion_handler_type and implements get(). The specialization tells ASIO which concrete handler type to construct and how to retrieve the final result.
How does ASIO determine which handler type to use for a custom token?
ASIO queries the completion_handler_type nested type alias within your async_result specialization. According to include/asio/async_result.hpp, the library uses decltype(std::declval<Token>()(std::declval<Handler>())) or an equivalent deduction to determine the exact handler type before constructing it.
Can custom tokens wrap other tokens like the built-in redirect_error?
Yes. You can implement adapter tokens that store an underlying token and transform the completion signature or handler invocation. Study include/asio/redirect_error.hpp to see how it wraps another token, modifies the handler's signature to remove error codes, and forwards to the wrapped token's handler factory.
Where should I place my custom token implementation files?
Follow the repository convention used by use_future and use_awaitable: place the public token definition in include/asio/your_token.hpp and the async_result specialization in include/asio/impl/your_token.hpp. This separation keeps interface and implementation distinct while ensuring the specialization is available when including the main async result header.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →