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:

  1. Deduce the handler type using typename asio::async_result<decay_t<CompletionToken>, Signature>::completion_handler_type
  2. Construct the handler by invoking the token's operator()(CompletionHandler), allowing the token to adapt or wrap the handler
  3. 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's operator())
  • 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:

Summary

  • Custom ASIO completion tokens require a token type implementing operator()(CompletionHandler) and an async_result specialization defining completion_handler_type and get().
  • 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.hpp and include/asio/impl/use_awaitable.hpp provide 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 in include/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →