tmc::select()#

select() awaits several awaitables and returns the result of the first one to complete, in a std::variant. As soon as the winner completes, each other awaitable (the losers) is cancelled, and their results are discarded. The variant’s index() tells you which awaitable won; its value holds that awaitable’s result (a void result becomes std::monostate).

Each awaitable must be paired with a way to cancel it, using tmc::cancellable. When the winner is decided, select() runs each loser’s canceller exactly once. A canceller may be synchronous or may itself return an awaitable (an async cancel), which select() awaits.

Contrast this with tmc::mux_many / tmc::mux_tuple, which deliver every result as it becomes ready and leave the other awaitables running. select() instead keeps only the first result and cancels the rest.

Up to 63 awaitables (31 on 32-bit) may be selected over.

API surface:

  • tmc::select() - await the awaitables and return the first result as a std::variant

  • tmc::cancellable - pair an awaitable with its cancellation action. Three forms are deduced automatically via CTAD:

    • tmc::cancellable(awaitable, functor) - a nullary functor that cancels when invoked (sync, or async if it returns an awaitable)

    • tmc::cancellable(awaitable, object) - an object exposing a .cancel() method (sync or async)

    • tmc::cancellable(object) - an object that is itself both the awaitable and exposes a .cancel() method (sync or async)

Usage Rules#

  • Every awaitable passed to select() must be wrapped in tmc::cancellable.

  • If several awaitables complete simultaneously, the winner is the one with the lowest index (leftmost argument); all others are cancelled.

  • A cancellation action must be safe to run even after its operation has already completed (it can race with completion), and should not throw.

  • select() waits for the cancelled losers to finish before returning, because they borrow storage from the calling coroutine frame. A canceller that cannot truly stop its operation will delay the return of select() until that operation completes on its own.

  • Do not pass an awaitable as the second argument to tmc::cancellable; pass a nullary functor that returns the awaitable, e.g. [&]{ return op.async_cancel(); }. It is invoked lazily, only when the cancel is actually needed.

Usage Examples#

Race an operation against a timeout timer. Whichever fires first wins, and select() cancels the loser automatically. Here both cancellable forms are shown: a lambda canceller for the operation, and a .cancel()-bearing object for the timer.

#include "tmc/asio/aw_asio.hpp"
#include "tmc/asio/ex_asio.hpp"
#include "tmc/select.hpp"
#include "tmc/task.hpp"

#include <asio/error_code.hpp>
#include <asio/steady_timer.hpp>
#include <chrono>
#include <variant>

// The operation is simulated here with a timer; in practice it would be a
// network read, a CPU-bound job with a cancellation flag, etc.
tmc::task<bool> run_with_timeout(std::chrono::milliseconds timeout) {
  asio::steady_timer operation{tmc::asio_executor(), std::chrono::seconds(1)};
  asio::steady_timer deadline{tmc::asio_executor(), timeout};

  // Slots correspond to argument order: index 0 is the operation,
  // index 1 is the timeout.
  std::variant<std::tuple<asio::error_code>, std::tuple<asio::error_code>> result =
    co_await tmc::select(
      // Form 1: a lambda that cancels the operation when invoked.
      tmc::cancellable(
        operation.async_wait(tmc::aw_asio), [&]() { operation.cancel(); }
      ),
      // Form 2: an object with a .cancel() method.
      tmc::cancellable(deadline.async_wait(tmc::aw_asio), deadline)
    );

  // true if the operation finished first; false if it timed out.
  co_return result.index() == 0;
}

Each awaitable is paired with a canceller via tmc::cancellable. The correct form is deduced from the arguments.

#include "tmc/select.hpp"

// Form 1: a nullary functor that cancels when invoked. It may run the cancel
// synchronously (returns void)...
tmc::cancellable(timer.async_wait(tmc::aw_asio), [&]{ timer.cancel(); });
// ...or lazily return an awaitable, which select() awaits (an async cancel).
tmc::cancellable(op.async_do(), [&]{ return op.async_cancel(); });

// Form 2: an object exposing a .cancel() method (sync or async). Asio I/O
// objects fit this form directly.
tmc::cancellable(timer.async_wait(tmc::aw_asio), timer);
tmc::cancellable(sock.async_read_some(buf, tmc::aw_asio), sock);

// Form 3: an object that is itself both the awaitable AND the cancel handle -
// it can be co_awaited and also exposes .cancel().
tmc::cancellable(op); // co_await op / op.cancel()

API Reference#

template<typename ...Awaitable, typename ...Canceller>
tmc::task<std::variant<tmc::detail::void_to_monostate<typename tmc::detail::get_awaitable_traits<Awaitable>::result_type>...>> tmc::select(tmc::cancellable<Awaitable, Canceller>... Pairs)#

Awaits all of the provided awaitables and returns the result of the first one to complete in a std::variant. The index() of the returned variant indicates which awaitable completed first; its value holds that awaitable’s result. A void-returning awaitable’s slot holds a std::monostate.

Each awaitable must be paired with a canceller using tmc::cancellable(). As soon as the first awaitable completes (the winner), the cancellers of all of the others (losers) are run; cancellers that return awaitables (async cancel) are awaited. Results from losers are discarded.

If multiple awaitables complete simultaneously, the winner is the awaitable with the lower index (leftmost parameter). All losers are cancelled, even those that may have already completed. Therefore, it must be safe to cancel a completed awaitable.

Implementation note: select() waits for cancelled losers to complete before returning. This is required since wrapped operations borrow storage from this coroutine frame, so they must all complete before it is destroyed. Consequently, a canceller that cannot truly stop its operation will delay the return of select() until that operation completes on its own.

template<typename Awaitable, typename Canceller>
class cancellable#

Pairs an Awaitable with a Canceller for use with tmc::select().

Construct one with class template argument deduction - tmc::cancellable(...) selects the correct specialization automatically. Three forms are supported:

  1. tmc::cancellable(awaitable, canceller) where canceller is a nullary functor that cancels the operation when invoked. It may return void (a sync cancel) or an awaitable (an async cancel, which select() awaits). The canceller is stored by value and invoked only for losers, so an awaitable-returning canceller builds its awaitable only when the cancel is actually needed. Therefore, it is safe to pass a capturing coroutine lambda here - it will be stored by value and outlive the awaited invocation.

    // sync cancel
    tmc::cancellable(timer.async_wait(tmc::aw_asio), [&]{ timer.cancel(); })
    
    // async cancel via a functor lazily returning an awaitable
    tmc::cancellable(op.async_do(), [&]{ return op.async_cancel(); })
    
    // async cancel via a functor lazily returning a task
    tmc::cancellable(op.async_do(), [&]() -> tmc::task<void> {
      co_await op.async_cancel();
    })
    

  2. tmc::cancellable(awaitable, object) where object exposes a .cancel() method. The .cancel() method may be sync or async (awaitable).

    // sync cancel - timer.cancel() runs synchronously (an Asio I/O object)
    tmc::cancellable(timer.async_wait(tmc::aw_asio), timer)
    
    // async cancel - op.cancel() returns an awaitable, which select() awaits
    tmc::cancellable(op.async_do(), op)
    

  3. tmc::cancellable(object) where object is itself both the awaitable and the cancellation handle: it can be co_awaited and also exposes a .cancel() method. The .cancel() method can be sync or async (awaitable); select() will automatically await it if needed. This constructor only accepts lvalues or non-movable rvalues. Movable rvalues are rejected to prevent lifetime issues.

    // sync cancel - op.cancel() runs synchronously
    tmc::cancellable(op)
    
    // async cancel - op2.cancel() returns an awaitable, which select() awaits
    tmc::cancellable(op2)
    

In every form, the cancellation action must be safe to run even after the operation has already completed (it may race with completion) and should not throw. select() runs the canceller of each losing awaitable exactly once.

Value category is forwarded throughout: a movable rvalue is owned by value; an lvalue or non-movable rvalue is borrowed (held by reference) and must outlive the tmc::select() call. Constructor CTAD handles this automatically.

Public Functions

template<typename A, typename C>
inline cancellable(A &&Aw, C &&Cancel)#

tmc::cancellable(awaitable, canceller) where canceller is a nullary functor that cancels the operation when invoked. It may return void (a sync cancel) or an awaitable (an async cancel, which select() awaits).

The canceller is stored by value and invoked only for losers, so an awaitable-returning canceller builds its awaitable only when the cancel is actually needed. Therefore, it is safe to pass a capturing coroutine lambda here - it will be stored by value and outlive the awaited invocation.

// sync cancel
tmc::cancellable(timer.async_wait(tmc::aw_asio), [&]{ timer.cancel(); })

// async cancel via a functor lazily returning an awaitable
tmc::cancellable(op.async_do(), [&]{ return op.async_cancel(); })

// async cancel via a functor lazily returning a task
tmc::cancellable(op.async_do(), [&]() -> tmc::task<void> {
  co_await op.async_cancel();
})