# 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<T>`, `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<T>` 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<int>` |
| `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<connection> {
    tcp::socket socket_;
    asio::strand<asio::any_io_executor> strand_;   // tcp::socket's executor is any_io_executor
    std::deque<std::string> 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<asio::steady_timer> 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.
