Skip to content

Ch22: Modern Control Flow — Coroutines & Modules

TL;DR: Coroutines & Suspended Execution: C++20 coroutines pause execution via co_await, co_yield, or co_return using a promise_type to manage state preservation and handle resumption. | std::format (C++20) gives Python-style, type-safe string formatting as a safer alternative to printf; <random> provides an engine + distribution pair that replaces rand() with statistically sound, seedable random number generation.

⚡ Quick Reference

#include <iostream>
#include <coroutine>

struct Task {
    struct promise_type {
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
    std::coroutine_handle<promise_type> coro_handle;
};

Task my_coroutine() {
    co_await std::suspend_always{};
    co_return;
}

int main() {
    Task t = my_coroutine();
    t.coro_handle.resume();
    bool completed = t.coro_handle.done();
    t.coro_handle.destroy();
    return 0;
}

Formatted Output & Random Distributions

#include <iostream>
#include <format>   // C++20 std::format (standard-library version of {fmt})
#include <random>

int main() {
    // std::format - type-safe, positional/named formatting, returns a std::string
    std::string msg = std::format("{} is {} years old", "Alice", 30);
    std::cout << msg << '\n';

    // Width, precision, and fill specifiers
    std::cout << std::format("{:>10.2f}", 3.14159) << '\n'; // right-aligned, 2 decimals

    // --- Random numbers ---
    // std::random_device - non-deterministic seed source (hardware entropy if available)
    std::random_device rd;

    // std::mt19937 - Mersenne Twister engine, seeded once, reused across calls
    std::mt19937 gen(rd());

    // std::uniform_int_distribution - maps engine output to a specific range/shape
    std::uniform_int_distribution<int> diceRoll(1, 6);
    std::cout << "Dice roll: " << diceRoll(gen) << '\n';

    std::uniform_real_distribution<double> percent(0.0, 100.0);
    std::cout << "Random %: " << percent(gen) << '\n';

    return 0;
}

🧠 Core Concepts

  • Coroutines & Suspended Execution: C++20 coroutines pause execution via co_await, co_yield, or co_return using a promise_type to manage state preservation and handle resumption.
  • Modular Compilation Units: Modules replace header files with compile-time isolation units (export module) that specify public interfaces via export, accelerating compile times.
  • Advanced Module Partitioning: Modules support hierarchical sub-modules (math.add_sub) and interface partitions (math:addition) to enable fine-grained, parallel translation compiles.
  • std::format vs printf: std::format (C++20, standardized from the third-party {fmt} library) checks format strings at compile time when possible, is type-safe (no %d/%s mismatches causing UB), and supports positional arguments and rich formatting specs like {:>10.2f}.
  • Engine + Distribution Separation: <random> deliberately splits "generate raw random bits" (the engine, e.g. std::mt19937) from "shape those bits into a useful range/distribution" (the distribution, e.g. std::uniform_int_distribution). This is why rand() is discouraged — it conflates both roles poorly and has known statistical bias.
  • Seeding: std::random_device provides a one-time, non-deterministic seed. The engine itself should be created once and reused for all subsequent random draws — recreating it (or reseeding) per-call is a common performance and correctness mistake.

⚠️ Pitfalls (Quick Scan)

Mistake Fix
Calling .resume() on a completed coroutine Always verify .done() returns false before resuming
Returning suspend_never from final_suspend() in custom generators Return suspend_always to inspect state safely post-completion
Placing #include directives after module declarations Always place standard headers inside the global module fragment (module; block)
Splitting template class implementations into separate implementation .cpp files Define all template class methods inline inside interface module files (.ixx)
Marking member variables or static definitions with export inside class scopes Apply export specifiers only to module-level namespace declarations
Using anonymous namespaces inside exported blocks Export individual namespace symbols directly at module level
Using printf-style %d/%s format strings with mismatched types Prefer std::format, which is compile-time checked and type-safe
Creating a new std::mt19937 engine every time you need a random number Construct the engine once, reuse it across all draws
Using std::random_device directly for repeated random draws It's meant only for seeding — use it once to seed an engine, then draw from the engine
Still using rand() % N for "random" ranges rand()'s low bits are less random and range isn't uniform — use <random> distributions instead
Assuming std::format mutates/prints directly It returns a std::string; you still need std::cout << or std::print (C++23) to output it

📖 Full Details

Cause → Effect → Fix with timestamp (click to expand) * **Calling `.resume()` on a completed coroutine** -> **Undefined behavior or severe memory corruption crashes** -> **Always verify `.done()` returns false before resuming (51:00)** * **Returning `suspend_never` from `final_suspend()` in custom generators** -> **Destroys coroutine frames immediately, turning handles dangling before inspection** -> **Return `suspend_always` to inspect state safely post-completion (53:00)** * **Placing `#include` directives after module declarations** -> **Compiler ignores includes or causes compile-time syntax errors** -> **Always place standard headers inside the global module fragment (`module;` block) (111:00)** * **Splitting template class implementations into separate implementation `.cpp` files** -> **Linker errors stating undefined reference because template code must be visible** -> **Define all template class methods inline inside interface module files (`.ixx`) (119:00)** * **Marking member variables or static definitions with `export` inside class scopes** -> **Compile-time syntax errors** -> **Apply `export` specifiers only to module-level namespace declarations (117:00)** * **Using anonymous namespaces inside exported blocks** -> **Compile-time error because anonymous namespace elements cannot be exported** -> **Export individual namespace symbols directly at module level (118:00)** * **Using `printf`-style format strings with mismatched argument types** -> **Undefined behavior at runtime since the compiler cannot verify `%d` actually receives an `int`, often silently printing garbage or crashing** -> **Use `std::format`, whose format string is checked against argument types (compile-time checked in C++20 when the format string is a constant expression)** * **Constructing a new `std::mt19937` engine on every random draw** -> **Each call reseeds from scratch, which is both slow (engine initialization has real cost) and can produce correlated/less-random sequences if seeded from a low-entropy or time-based source repeatedly** -> **Create the engine once (e.g. as a local static or class member) and reuse it for every subsequent draw** * **Calling `std::random_device` repeatedly for each random number instead of just for seeding** -> **`random_device` is often slow (may query OS-level entropy) and isn't intended as a general-purpose generator** -> **Use `random_device` exactly once to seed a `std::mt19937` (or similar engine), then draw all further numbers from that engine** * **Continuing to use legacy `rand() % N` for generating a bounded random value** -> **The modulo operation introduces statistical bias (lower values are slightly more likely when `RAND_MAX + 1` isn't a multiple of `N`), and `rand()`'s lower bits have known poor randomness in many implementations** -> **Use `std::uniform_int_distribution(min, max)` paired with a proper engine instead** * **Expecting `std::format(...)` to print output directly like `printf` does** -> **`std::format` returns a `std::string` object; calling it alone produces no visible output and the formatted text is simply discarded if unused** -> **Wrap the call in `std::cout << std::format(...)`, or use `std::print(...)` (C++23) which does output directly**

📎 Repo Files

  • 48.Coroutines/47.5CoAwait/main.cpp
  • 48.Coroutines/47.6CoYield/main.cpp
  • 48.Coroutines/47.7CoReturn/main.cpp
  • 48.Coroutines/47.8CustomGenerator/main.cpp
  • 48.Coroutines/47.9ThirdPartyCorotineTypes/main.cpp
  • 49.Modules/48.1Introduction/main.cpp
  • 49.Modules/48.2YourFirstModule/main.cpp
  • 49.Modules/48.3BlockExport/main.cpp
  • 49.Modules/48.4SeparateInterfaceFromImplementation-SameFile/main.cpp
  • 49.Modules/48.5SeparateInterfaceFromImplementation-DifferentFiles/main.cpp
  • 49.Modules/48.6MultipleImplementationFiles/main.cpp
  • 49.Modules/48.7MultipleInterfaceFiles/main.cpp
  • 49.Modules/48.8ExportImport/main.cpp
  • 49.Modules/48.9SubModules/main.cpp
  • 49.Modules/48.10ModuleInterfacePartitions/main.cpp
  • 49.Modules/48.11BoxContainerModule/main.cpp
  • 49.Modules/48.12ModulesWithNamespaces/main.cpp
  • 49.Modules/48.13VisibilityReachability/main.cpp
  • 49.Modules/48.14PrivateModuleFragment/main.cpp
  • 49.Modules/48.15DosAndDonts/main.cpp
  • (No dedicated repo folder yet — supplementary topic added from gap analysis)