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:
- Static query check – Uses
static_requirefrominclude/asio/traits/static_require.hppto determine if the property is a static property. Ifstatic_query_vis valid, the CPO returns the identity result. - Member function detection – Uses
require_memberorprefer_membertraits (defined ininclude/asio/traits/require_member.hppandinclude/asio/traits/prefer_member.hpp) to detect if the type exposes a matching member function. - Free function discovery – Uses
require_freeorprefer_freetraits (frominclude/asio/traits/require_free.hppandinclude/asio/traits/prefer_free.hpp) to find free functions via ADL. - Recursive chaining – When multiple properties are supplied, the CPO recursively applies each property to the result of the previous call using
two_propsandn_propsoverloads. - Ill-formed fallback – For
require, if no valid overload exists, the expression is ill-formed. Forprefer, 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_memberandprefer_memberdetect member functions with the signaturerequire(Prop)orprefer(Prop).require_freeandprefer_freedetect free functions via ADL with signaturesrequire(E, P)orprefer(E, P).
Result and Noexcept Traits
For generic programming, ASIO provides result type and exception specification traits:
can_require<T, Props...>andcan_prefer<T, Props...>–trueif the expression is well-formed.is_nothrow_require<T, Props...>andis_nothrow_prefer<T, Props...>–trueif the selected overload isnoexcept.require_result<T, Props...>::typeandprefer_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::requireenforces mandatory properties through a hierarchy of static queries, member functions, and free functions, failing compilation if the property cannot be satisfied.asio::preferattempts property application with graceful fallback to the original executor, never causing compile-time errors.- The resolution order is implemented in
include/asio/require.hppandinclude/asio/prefer.hppusingcall_traitsthat prioritize static requirements, then member functions, then ADL-discovered free functions. - Support traits in
include/asio/traits/provide the SFINAE-based detection mechanisms forrequire_member,require_free,prefer_member, andprefer_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →