Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ jobs:
if: ${{ !matrix.coverage && !matrix.time-trace && !matrix.superproject-cmake && !matrix.clang-tidy }}
env:
ASAN_OPTIONS: ${{ ((matrix.compiler == 'apple-clang' || matrix.compiler == 'clang' || matrix.compiler == 'mingw') && 'detect_invalid_pointer_pairs=0:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1') || 'detect_invalid_pointer_pairs=2:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1' }}
TSAN_OPTIONS: ${{ matrix.tsan && 'halt_on_error=1:second_deadlock_stack=1' || '' }}
# Relative to the b2 working directory (boost-root); absolute
# workspace paths differ between container and host runners.
TSAN_OPTIONS: ${{ matrix.tsan && 'halt_on_error=1:second_deadlock_stack=1:suppressions=libs/capy/test/tsan.supp' || '' }}
with:
source-dir: boost-root
modules: capy
Expand All @@ -190,7 +192,7 @@ jobs:
generator: ${{ matrix.generator }}
generator-toolset: ${{ matrix.generator-toolset }}
build-type: ${{ matrix.build-type }}
build-target: boost_capy_tests
build-target: tests
run-tests: true
cxxstd: ${{ matrix.cmake-cxxstd || matrix.cxxstd }}
cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }}
Expand Down
8 changes: 8 additions & 0 deletions doc/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ asciidoc:
nav:
- modules/ROOT/nav.adoc
ext:
collector:
- scan:
- dir: example
into: modules/ROOT/examples
- dir: test/doc/snippets
into: modules/ROOT/examples/snippets
- dir: test/doc/programs
into: modules/ROOT/examples/programs
cpp-reference:
config: doc/mrdocs.yml
cpp-tagfiles:
Expand Down
1 change: 1 addition & 0 deletions doc/local-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ui:

antora:
extensions:
- require: '@antora/collector-extension'
- require: '@sntke/antora-mermaid-extension'
mermaid_library_url: https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs
script_stem: header-scripts
Expand Down
39 changes: 5 additions & 34 deletions doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ Consider this function:

[source,cpp]
----
int compute(int x, int y)
{
int result = x * y + 42;
return result;
}
include::example$snippets/2a_foundations.cpp[tag=compute,indent=0]
----

When `compute` is called:
Expand Down Expand Up @@ -57,7 +53,7 @@ When a coroutine resumes:

This capability is implemented through a *coroutine frame*—a heap-allocated block of memory that stores the coroutine's state. Unlike stack frames, coroutine frames persist across suspension points because they live on the heap rather than the stack.

[source,cpp]
[source,cpp,role=pseudocode]
----
// Conceptual illustration (not real syntax)
task<int> fetch_and_process()
Expand All @@ -80,14 +76,7 @@ In traditional synchronous code:

[source,cpp]
----
void handle_request(connection& conn)
{
std::string request = conn.read(); // blocks until data arrives
auto parsed = parse_request(request);
auto data = database.query(parsed.id); // blocks until database responds
auto response = compute_response(data);
conn.write(response); // blocks until write completes
}
include::example$snippets/2a_foundations.cpp[tag=sync_request,indent=0]
----

This code reads naturally from top to bottom. But while waiting for the network or database, this function blocks the entire thread. If you have thousands of concurrent connections, you would need thousands of threads, each consuming memory and requiring operating system scheduling.
Expand All @@ -98,18 +87,7 @@ The traditional solution uses callbacks:

[source,cpp]
----
void handle_request(connection& conn)
{
conn.async_read([&conn](std::string request) {
auto parsed = parse_request(request);
database.async_query(parsed.id, [&conn](auto data) {
auto response = compute_response(data);
conn.async_write(response, [&conn]() {
// request complete
});
});
});
}
include::example$snippets/2a_foundations.cpp[tag=callback_request,indent=0]
----

This code does not block. Each operation starts, registers a callback, and returns immediately. When the operation completes, the callback runs.
Expand All @@ -122,14 +100,7 @@ Coroutines restore linear code structure while maintaining asynchronous behavior

[source,cpp]
----
task<void> handle_request(connection& conn)
{
std::string request = co_await conn.async_read();
auto parsed = parse_request(request);
auto data = co_await database.async_query(parsed.id);
auto response = compute_response(data);
co_await conn.async_write(response);
}
include::example$snippets/2a_foundations.cpp[tag=coroutine_request,indent=0]
----

This code reads like the original blocking version. Local variables like `request`, `parsed`, and `data` exist naturally in their scope. Yet the function suspends at each `co_await` point, allowing other work to proceed while waiting.
Expand Down
99 changes: 6 additions & 93 deletions doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ The `co_await` keyword suspends the coroutine and waits for some operation to co

[source,cpp]
----
task<std::string> fetch_page(std::string url)
{
auto response = co_await http_get(url); // suspends until HTTP completes
return response.body; // continues after resumption
}
include::example$snippets/2b_syntax.cpp[tag=co_await_example,indent=0]
----

=== co_yield
Expand All @@ -30,13 +26,7 @@ The `co_yield` keyword produces a value and suspends the coroutine. This pattern

[source,cpp]
----
generator<int> count_to(int n)
{
for (int i = 1; i <= n; ++i)
{
co_yield i; // produce value, suspend, resume when next value requested
}
}
include::example$snippets/2b_syntax.cpp[tag=co_yield_example,indent=0]
----

=== co_return
Expand All @@ -45,11 +35,7 @@ The `co_return` keyword completes the coroutine and optionally provides a final

[source,cpp]
----
task<int> compute()
{
int result = 42;
co_return result; // completes the coroutine with value 42
}
include::example$snippets/2b_syntax.cpp[tag=co_return_example,indent=0]
----

For coroutines that do not return a value, use `co_return;` without an argument.
Expand All @@ -65,24 +51,7 @@ Here is the minimal structure needed to create a coroutine:

[source,cpp]
----
#include <coroutine>

struct SimpleCoroutine
{
struct promise_type
{
SimpleCoroutine get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};

SimpleCoroutine my_first_coroutine()
{
co_return; // This makes it a coroutine
}
include::example$snippets/2b_syntax.cpp[tag=simple_coroutine,indent=0]
----

The `promise_type` nested structure provides the minimum scaffolding the compiler needs. You will learn what each method does in xref:2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery].
Expand All @@ -101,59 +70,7 @@ When you write `co_await expr`, the expression `expr` must be an *awaitable*—s

[source,cpp]
----
#include <coroutine>
#include <iostream>

struct ReturnObject
{
struct promise_type
{
ReturnObject get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};

struct Awaiter
{
std::coroutine_handle<>* handle_out;

bool await_ready() { return false; } // always suspend

void await_suspend(std::coroutine_handle<> h)
{
*handle_out = h; // store handle for later resumption
}

void await_resume() {} // nothing to return
};

ReturnObject counter(std::coroutine_handle<>* handle)
{
Awaiter awaiter{handle};

for (unsigned i = 0; ; ++i)
{
std::cout << "counter: " << i << std::endl;
co_await awaiter;
}
}

int main()
{
std::coroutine_handle<> h;
counter(&h);

for (int i = 0; i < 3; ++i)
{
std::cout << "main: resuming" << std::endl;
h();
}

h.destroy();
}
include::example$programs/2b_syntax_counter.cpp[tag=full]
----

*Output:*
Expand Down Expand Up @@ -192,11 +109,7 @@ These are useful building blocks for promise types and custom awaitables.

[source,cpp]
----
// suspend_always causes suspension at this point
co_await std::suspend_always{};

// suspend_never continues immediately without suspending
co_await std::suspend_never{};
include::example$snippets/2b_syntax.cpp[tag=standard_awaiters,indent=0]
----

You have now learned the three coroutine keywords and how awaitables work. In the next section, you will learn about the promise type and coroutine handle—the machinery that makes coroutines function.
Loading
Loading