ASIO Executor Customization with `require` and `prefer` Concepts: A Complete Guide

ASIO's require and prefer customization-point objects let you modify executors by applying properties through a compile-time resolution hierarchy that falls back gracefully when properties cannot be satisfied.

The chriskohlhoff/asio repository implements a sophisticated executor customization model using C++ customization-point objects (CPOs). These mechanisms enable generic code to request executor modifications without knowing the concrete executor type, leveraging compile-time traits to select the optimal implementation strategy.

How require and prefer Work

ASIO provides two primary customization points for executor modification. Both are defined as CPOs that use template metaprogramming to resolve the best available implementation at compile time.

The require CPO

asio::require enforces properties that must be satisfied. When invoked in include/asio/require.hpp, it attempts to transform an executor by applying a specific property. If the property cannot be satisfied, the expression is ill-formed and produces a compile-time error.

The implementation uses call_traits structs to build a resolution hierarchy. First, it checks if the property satisfies static_require via a static query. If valid, it returns the original executor unchanged (the identity overload). Otherwise, it attempts to detect a member require function, then falls back to free functions found via ADL.

The prefer CPO

asio::prefer attempts to apply properties but allows graceful degradation. Defined in include/asio/prefer.hpp, it mirrors the require logic but adds a critical fallback: when a property is not directly preferable, it attempts the same mechanisms as require before finally invoking the identity overload.

This means prefer never produces a hard error. If the property cannot be applied through any mechanism, it simply returns the original executor unchanged, enabling code to request optimizations without requiring their presence.

The Compile-Time Resolution Hierarchy

Both CPOs follow a strict concept-preserving resolution order encoded in their respective call_traits implementations:

  1. Static query check – Uses static_require from include/asio/traits/static_require.hpp to determine if the property is a static property. If static_query_v is valid, the CPO returns the identity result.
  2. Member function detection – Uses require_member or prefer_member traits (defined in include/asio/traits/require_member.hpp and include/asio/traits/prefer_member.hpp) to detect if the type exposes a matching member function.
  3. Free function discovery – Uses require_free or prefer_free traits (from include/asio/traits/require_free.hpp and include/asio/traits/prefer_free.hpp) to find free functions via ADL.
  4. Recursive chaining – When multiple properties are supplied, the CPO recursively applies each property to the result of the previous call using two_props and n_props overloads.
  5. Ill-formed fallback – For require, if no valid overload exists, the expression is ill-formed. For prefer, the identity overload is always available as a final fallback.

Core Traits and Type Support

The customization mechanism relies on traits defined in the include/asio/traits/ directory and include/asio/is_applicable_property.hpp.

Applicability and Static Requirements

is_applicable_property<T, P> determines if property P can be queried on type T. This is the first gate checked before attempting any customization.

static_require<T, P> checks whether a static query is valid for the property. When true, the CPO bypasses all runtime mechanisms and returns the executor unchanged, providing zero-overhead property application for static metadata.

Member and Free Function Detection

The traits system uses SFINAE to detect implementation strategies:

  • require_member and prefer_member detect member functions with the signature require(Prop) or prefer(Prop).
  • require_free and prefer_free detect free functions via ADL with signatures require(E, P) or prefer(E, P).

Result and Noexcept Traits

For generic programming, ASIO provides result type and exception specification traits:

  • can_require<T, Props...> and can_prefer<T, Props...> – true if the expression is well-formed.
  • is_nothrow_require<T, Props...> and is_nothrow_prefer<T, Props...> – true if the selected overload is noexcept.
  • require_result<T, Props...>::type and prefer_result<T, Props...>::type – expose the resulting type after applying properties.

These traits enable static_assert validations and noexcept specifications in generic executor wrappers.

Practical Examples of ASIO Executor Customization

Basic require with Static Properties

When a property is static, require returns the original executor without modification:

#include <asio.hpp>
#include <type_traits>

using namespace asio;

int main() {
    auto ex = make_strand(io_context{});
    auto ex_req = require(ex, execution::blocking.never);
    
    // Static property: no change in type
    static_assert(std::is_same_v<decltype(ex_req), decltype(ex)>);
}

This invokes the identity overload because execution::blocking.never satisfies static_require in include/asio/traits/static_require.hpp.

Member Function Fallback

Executors can implement member functions to handle property requests:

struct my_executor {
    using execution_context = void;
    
    template <typename Prop>
    my_executor require(Prop) const noexcept { 
        return *this; 
    }
};

int main() {
    my_executor exec;
    auto exec2 = require(exec, execution::outstanding_work.tracked);
    // Calls require_member overload via asio::require CPO
}

The require CPO detects the member function using require_member traits and forwards the call accordingly.

Graceful Degradation with prefer

Use prefer when a property is optional:

auto ex = make_strand(io_context{});
auto ex_preferred = prefer(ex, execution::outstanding_work.untracked);

If untracked is not marked as is_preferable and no require mechanism exists, the CPO returns ex unchanged rather than failing compilation.

Chaining Multiple Properties

Both CPOs support variadic property application:

auto ex = make_strand(io_context{});
auto ex_mod = prefer(
    require(ex, execution::blocking.always),
    execution::outstanding_work.tracked,
    execution::relationship.fork);

The implementation recursively applies each property, using the result of the previous transformation as input to the next.

Compile-Time Verification

Validate executor capabilities before use:

static_assert(can_require_v<decltype(ex), execution::blocking.never>);
static_assert(!can_prefer_v<decltype(ex), execution::blocking.never>);
static_assert(is_nothrow_prefer_v<decltype(ex), execution::outstanding_work.tracked>);

These assertions use the traits defined in require.hpp and prefer.hpp to verify interface compliance.

Summary

  • asio::require enforces mandatory properties through a hierarchy of static queries, member functions, and free functions, failing compilation if the property cannot be satisfied.
  • asio::prefer attempts property application with graceful fallback to the original executor, never causing compile-time errors.
  • The resolution order is implemented in include/asio/require.hpp and include/asio/prefer.hpp using call_traits that prioritize static requirements, then member functions, then ADL-discovered free functions.
  • Support traits in include/asio/traits/ provide the SFINAE-based detection mechanisms for require_member, require_free, prefer_member, and prefer_free.
  • Result type traits (require_result, prefer_result) and capability checks (can_require, can_prefer) enable robust generic programming against ASIO executors.

Frequently Asked Questions

What is the difference between asio::require and asio::prefer?

asio::require demands that a property be satisfied and produces a compile-time error if it cannot be applied, making it suitable for essential executor characteristics. asio::prefer attempts to apply a property but falls back to returning the original executor unchanged if the property is unavailable, making it ideal for optional optimizations that should not break compilation when unsupported.

How does ASIO detect which require overload to use?

The asio::require CPO in include/asio/require.hpp uses a series of call_traits to test conditions in order: first checking static_require for static properties, then using require_member traits to detect member functions, then require_free traits to find free functions via ADL, and finally supporting variadic chaining. This compile-time dispatch ensures the most efficient implementation is selected without runtime overhead.

What happens when a property cannot be applied to an executor?

When using require, the expression is ill-formed and compilation fails, enforcing that the executor must support the requested property. When using prefer, if no static query, member function, or free function can satisfy the property, the CPO invokes the identity overload and returns the original executor unchanged, effectively ignoring the request.

Where are the customization point implementations defined?

The primary CPOs are defined in include/asio/require.hpp and include/asio/prefer.hpp. The supporting traits for detecting member and free functions reside in include/asio/traits/require_member.hpp, include/asio/traits/require_free.hpp, include/asio/traits/prefer_member.hpp, and include/asio/traits/prefer_free.hpp. Static property support is implemented in include/asio/traits/static_require.hpp, and general applicability checks are in include/asio/is_applicable_property.hpp.

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 →