# Pre-C++20 Styles (C++11–17, Boost ≥ 1.74) If you can't use C++20 `co_await`, the **same Asio library** (modern Boost or standalone) still works — only the *async style* changes. Compile with C++11 or later. Two pre-C++20 styles: 1. **Completion handlers (callbacks)** — header-only, C++11, no extra dependencies. The recommended baseline. 2. **Stackful coroutines** (`asio::spawn` + `yield_context`) — synchronous-looking like `co_await`, but built on Boost.Coroutine/Boost.Context, so it **must be linked** (not header-only) — see build note below. **Unchanged from the coroutine style** (these are library, not language, features): `io_context`, `make_strand`, `bind_executor`, `steady_timer`, `ssl::stream`, `signal_set`, `async_read`/`async_write`/`async_read_until`, buffers, `resolver`. Use them exactly as shown in [coroutines.md](coroutines.md). **Not available pre-C++20:** `co_await`/`awaitable`, `co_spawn`, `use_awaitable`, the `||`/`&&` `awaitable_operators`, `as_tuple`, and `co_composed`. The table below gives the equivalent. ### C++20 → pre-C++20 mapping | C++20 coroutine | Pre-C++20 equivalent | |-----------------|----------------------| | `co_await op(use_awaitable)` | callback: `op(handler)` · stackful: `op(yield)` | | `awaitable` function | member `do_x()` callback chain · or `spawn(strand, fn)` | | `co_spawn(ex, coro, tok)` | start the callback chain · or `asio::spawn(ex, fn, tok)` | | `as_tuple(use_awaitable)` → `[ec,n]` | callback's `(ec, n)` params · stackful: `op(yield[ec])` | | `a() \|\| b()` (first-wins race) | a **watchdog timer** that closes the socket; the other op fails with `operation_aborted` | | `a() && b()` (wait both) | launch both, count completions in a shared `shared_ptr` | | `co_composed<>` custom op | `asio::async_compose<>` (C++11) | | `co_await this_coro::executor` | `socket_.get_executor()` / a passed-in executor | ### Callback style: full-duplex + write queue The full-duplex write-queue rule is identical — a strand alone doesn't stop interleaved writes — just expressed with chained handlers. Capture `self = shared_from_this()` in **every** handler to keep the connection alive. ```cpp class connection : public std::enable_shared_from_this { tcp::socket socket_; asio::strand strand_; // tcp::socket's executor is any_io_executor std::deque outbox_; bool writing_ = false; char buf_[1024]; public: explicit connection(tcp::socket s) : socket_(std::move(s)), strand_(asio::make_strand(socket_.get_executor())) {} void start() { do_read(); } void send(std::string frame) { // call on the strand only outbox_.push_back(std::move(frame)); if (!writing_) do_write(); } private: void do_read() { auto self = shared_from_this(); socket_.async_read_some(asio::buffer(buf_), asio::bind_executor(strand_, // serialize handler execution [this, self](boost::system::error_code ec, std::size_t n) { if (ec) return; // self drops here → socket closes /* parse buf_[0..n]; call send() for replies */ do_read(); })); } void do_write() { // at most one async_write in flight writing_ = true; auto self = shared_from_this(); asio::async_write(socket_, asio::buffer(outbox_.front()), asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t) { if (ec) { writing_ = false; return; } outbox_.pop_front(); if (!outbox_.empty()) do_write(); else writing_ = false; })); } }; ``` ### Stackful style: spawn + yield_context `yield` is a completion token: `op(socket, ..., yield)` suspends until done and returns the result; errors **throw** by default, or use `yield[ec]` for an `error_code`. Run each chain on a per-connection strand. ```cpp asio::spawn(strand, // executor or strand [self](asio::yield_context yield) { // capture self for lifetime try { char data[1024]; for (;;) { std::size_t n = self->socket_.async_read_some(asio::buffer(data), yield); asio::async_write(self->socket_, asio::buffer(data, n), yield); } } catch (const std::exception&) { self->socket_.close(); } }, asio::detached); // completion token (3rd arg) ``` ### Timeout without `||` (watchdog timer) Replace the `read || timer` race with a separate watchdog: reset the timer on each read; a second chain waits on it and closes the socket on expiry, which makes the read fail with `operation_aborted`. ```cpp // callback watchdog void arm_timeout() { timer_.expires_after(std::chrono::seconds(30)); auto self = shared_from_this(); timer_.async_wait(asio::bind_executor(strand_, [this, self](boost::system::error_code ec) { if (!ec) socket_.close(); // fired → drop; reset cancels with ec })); } // call arm_timeout() again on every frame received to re-arm ``` ### Callback multi-step reads + recurring side-tasks **Lifetime shift:** coroutine *stack locals* become *member variables* in callback style — a header/body buffer must outlive each async op or it dangles. Chain a composed read of the length, then the body: ```cpp // members, NOT locals — they must survive until the handler runs uint32_t len_be_; std::string body_; void read_frame() { auto self = shared_from_this(); asio::async_read(socket_, asio::buffer(&len_be_, sizeof len_be_), asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t) { if (ec) return; body_.assign(ntohl(len_be_), '\0'); asio::async_read(socket_, asio::buffer(body_), // read exactly N bytes asio::bind_executor(strand_, [this, self](boost::system::error_code ec2, std::size_t) { if (ec2) return; handle_frame(body_); // dispatch on type byte read_frame(); // next frame })); })); } ``` **Recurring side-task** (e.g. push every 250ms) running concurrently with the read loop — there is no detached coroutine to stop, so use a self-rescheduling timer and `cancel()` it to stop: ```cpp void schedule_tick(std::string symbol, std::shared_ptr t) { t->expires_after(std::chrono::milliseconds(250)); auto self = shared_from_this(); t->async_wait(asio::bind_executor(strand_, [this, self, symbol, t](boost::system::error_code ec) { if (ec) return; // cancelled on unsubscribe/close → stops send(make_tick(symbol)); // enqueue on the write queue schedule_tick(symbol, t); // reschedule itself })); } // start: keep one timer per subscription alive (e.g. in a map); stop: erase + t->cancel() ``` ### Build difference (stackful spawn only) Callbacks need no change beyond the standard (drop `-fcoroutines`; it's only for C++20 `co_await`): ```cmake set(CMAKE_CXX_STANDARD 11) # or 14 / 17 set(CMAKE_CXX_EXTENSIONS OFF) # else CMake emits -std=gnu++NN, not literal -std=c++NN target_link_libraries(app PRIVATE Boost::headers Threads::Threads) target_compile_definitions(app PRIVATE BOOST_ERROR_CODE_HEADER_ONLY) ``` Stackful `spawn` additionally requires Boost.Coroutine (which uses Boost.Context) — **not header-only**: ```cmake find_package(Boost REQUIRED COMPONENTS coroutine) target_link_libraries(app PRIVATE Boost::coroutine) # pulls in Boost.Context ``` > Standalone Asio's `spawn` also depends on Boost.Coroutine/Context — it drags Boost into an otherwise Boost-free build. If you want zero Boost, use the **callback** style. > > The 3-arg `spawn(ex, fn, token)` form needs **Boost ≥ 1.80** (older Boost has only `spawn(ex, fn)`). On old distros like Debian bookworm (Boost 1.74), the **callback** style compiles cleanly while stackful `spawn` does not — verified.