cpp-coroutines について
C++開発者向けのC++20コルーチンガイドです。キーワード、promise_type、一時停止と再開、フレームメモリ、GDBでのデバッグを解説します。ジェネレーター、Boost.Asioでの利用、フレーム割り当ての削減も扱います。
主な機能
- 01co_await、co_yield、co_returnの理解
- 02コルーチン型とpromise_typeの実装
- 03ジェネレーターとawaitableの作成
- 04コルーチンフレームの構造とメモリの確認
- 05GDBによる一時停止中のコルーチンのデバッグ
- 06よくある問題の把握と割り当て負荷の削減
活用シーン
- 01非同期タスク向けのC++20コルーチン型の実装
- 02co_yieldを使った遅延シーケンス生成器の作成
- 03一時停止中の状態とフレームメモリの確認
- 04ハンドルの寿命や二重再開の問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mohitmishra786/ low-level-dev-skills --skill cpp-coroutinesインストール後、Claude Code で/cpp-coroutinesを入力して読み込まれたか確認します。
詳細
C++20 コルーチン
目的
C++20 のコルーチンの仕組みについてエージェントをガイドします。対象は、co_await、co_yield、co_return、必須の promise_type の実装、コルーチンフレームのメモリレイアウトの理解、GDB での中断中のコルーチンのデバッグ、フレーム割り当てのオーバーヘッドの削減です。
トリガー
- 「co_await、co_yield、co_return はどのように動作しますか?」
- 「コルーチンの promise_type はどのように実装しますか?」
- 「コルーチンはどのように中断・再開しますか?」
- 「GDB で中断中のコルーチンをデバッグするにはどうすればよいですか?」
- 「コルーチンフレームはどれくらいのメモリを使用しますか?」
- 「co_yield を使ったジェネレーターはどのように記述しますか?」
ワークフロー
1. 3 つのコルーチンキーワード
// co_return — return a value and end the coroutine
co_return value;
// co_yield — produce a value, suspend, resume later
co_yield value;
// co_await — suspend until an awaitable completes
auto result = co_await some_awaitable;
次の 3 つのキーワードのいずれかを含む関数はコルーチンです。その戻り値の型は、promise_type を持つコルーチン型でなければなりません。
2. 最小限のコルーチン型 — Task
#include <coroutine>
#include <stdexcept>
#include <optional>
template <typename T>
struct Task {
struct promise_type {
std::optional<T> value;
std::exception_ptr exception;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; } // lazy start
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
Task(Task&&) = default;
Task& operator=(Task&&) = default;
~Task() { if (handle) handle.destroy(); }
T get() {
handle.resume(); // resume to completion
if (handle.promise().exception)
std::rethrow_exception(handle.promise().exception);
return std::move(*handle.promise().value);
}
};
// Usage
Task<int> compute() {
co_return 42;
}
int main() {
auto task = compute();
int result = task.get(); // 42
}
3. co_yield を使ったジェネレーター
template <typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{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() { throw; }
std::suspend_always yield_value(T value) {
current_value = value;
return {}; // suspend after yielding
}
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
struct iterator {
std::coroutine_handle<promise_type> handle;
bool done;
iterator& operator++() {
handle.resume();
done = handle.done();
return *this;
}
T operator*() const { return handle.promise().current_value; }
bool operator!=(std::default_sentinel_t) const { return !done; }
};
iterator begin() {
handle.resume(); // advance to first yield
return {handle, handle.done()};
}
std::default_sentinel_t end() { return {}; }
};
// Usage
Generator<int> iota(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
for (int x : iota(0, 5)) {
std::cout << x << ' '; // 0 1 2 3 4
}
4. Awaitable — カスタム co_await 対象
// An awaitable has three methods:
// await_ready() — true means don't suspend
// await_suspend(handle) — suspend: store handle, schedule resume
// await_resume() — return value of co_await expression
struct TimerAwaitable {
int delay_ms;
bool await_ready() const noexcept { return delay_ms <= 0; }
void await_suspend(std::coroutine_handle<> h) {
// Schedule h.resume() to be called after delay
std::thread([h, this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
h.resume();
}).detach();
}
void await_resume() const noexcept {} // no return value
};
// suspend_always and suspend_never are built-in awaitables
std::suspend_always{}; // always suspends
std::suspend_never{}; // never suspends (no-op)
5. コルーチンフレームのレイアウトとメモリ
コンパイラーは、次の要素を含むコルーチンフレーム(ヒープオブジェクト)を割り当てます。
- 中断ポイントをまたいで存続するローカル変数
- promise オブジェクト
- 現在の中断状態(再開位置)
- 再開関数/破棄関数へのポインター
// Inspect frame size with Compiler Explorer (godbolt.org)
// Compile with: g++ -std=c++20 -O2 -S
// Look for: operator new call size in the generated asm
// Or: clang -std=c++20 -O2 -emit-llvm -S | grep "coro.size"
// Reduce frame size:
// 1. Don't keep large objects alive across co_await
struct Bad {
std::vector<char> large_buf; // whole vector lives in frame
co_return large_buf.size(); // large_buf crosses suspension
};
// 2. Move data out before suspending
std::vector<char> buf = get_data();
size_t sz = buf.size(); // capture only what's needed
buf.clear(); // release before suspension
co_await next_event;
// sz still valid; buf released
6. GDB で中断中のコルーチンをデバッグする
# Coroutines appear as regular stack frames after resume()
# To inspect a suspended coroutine:
(gdb) info locals
# Look for coroutine_handle variables
# Print the promise object
(gdb) p *(promise_type*)(handle.__handle_)
# GDB 14+ has coroutine-specific support
(gdb) info coroutines # GCC coroutine support (experimental)
# Step through coroutine execution
(gdb) step # enters co_await implementation
(gdb) finish # returns from coroutine frame function
(gdb) next # step over suspension point
# View all threads (coroutines running on thread pool)
(gdb) info threads
(gdb) thread 2
(gdb) bt
7. Boost.Asio の co_spawn と co_await
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/awaitable.hpp>
namespace net = boost::asio;
net::awaitable<void> echo_session(net::ip::tcp::socket socket) {
char buf[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(net::buffer(buf));
co_await net::async_write(socket, net::buffer(buf, n));
}
}
int main() {
net::io_context io;
net::co_spawn(io, listen_accept(io), net::detached);
io.run();
}
co_spawn はエグゼキューター上でコルーチンを起動します。co_await はコールバックのネストなしに完了トークンを連結します。
8. std::generator(C++23)
#include <generator>
#include <ranges>
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// Usage
for (int v : fibonacci() | std::views::take(10))
printf("%d\n", v);
手動でコルーチンハンドルを管理せずに遅延シーケンスを扱えます。コンパイラーが std::generator の promise 型を提供します。
9. GDB でのコルーチンフレームのレイアウト
# Compile with debug info
g++ -std=c++20 -g -O0 -o app app.cpp
gdb ./app
(gdb) break my_coro
(gdb) run
(gdb) info frame # current stack frame
(gdb) info locals # promise, handle in scope
# Inspect coroutine frame pointer (compiler-specific mangling)
(gdb) p *(MyPromise*)h.address() # h = coroutine_handle
# GCC coroutine support (GCC 14+)
(gdb) info coroutines
# Pretty-print promise state
(gdb) set print pretty on
(gdb) p promise
中断中のコルーチンは、再開されるまでスタック上に表示されない場合があります。保存された coroutine_handle を使って追跡してください。
10. コンパイル時間への影響
コルーチンはテンプレートのインスタンス化とヘッダーの解析コストを増加させます。
| 軽減策 | 効果 |
|---|---|
-O2 HALO | 生成されるフレーム関連コードを削減します |
.cpp でのアウトオブライン co_await | 再コンパイルの連鎖を抑えます |
| コルーチン戻り値型への Pimpl | ヘッダーから awaitable テンプレートを隠します |
ccache / モジュール | C++ では skills/rust/rust-build-times のパターンを参照してください |
g++ -ftime-report または clang -ftime-trace で計測します。コルーチンを多用するヘッダー(Asio)では、RAM 使用量とのバランスを取りながら、ユニティビルドを限定的に活用すると効果があります。
11. よくある落とし穴
| 問題 | 原因 | 対処法 |
|---|---|---|
非コルーチン内での co_await | 関数にコルーチンの戻り値型が指定されていない | 戻り値の型をコルーチン型に変更します |
co_return 後のダングリングハンドル | コルーチン終了後にハンドルを使用している | 再開前に handle.done() を確認します |
| 二重再開 | すでに再開されたコルーチンを再開している | 状態を追跡し、中断中の場合にのみ再開します |
| コルーチンフレームが解放されない | handle.destroy() の呼び出し忘れ | RAII ラッパー(Task、Generator)を使用します |
| ヒープ割り当てのオーバーヘッド | コルーチン呼び出しごとに新しいフレームが割り当てられる | -O2 で HALO(Heap Allocation eLision Optimization)を有効にします |
| 再帰的な co_await の深さ | 深いチェーンによるスタックオーバーフロー | std::coroutine_handle<> の末尾呼び出しパターンを使用します |
関連スキル
- その他の高度な C++20 機能については、
skills/compilers/cpp-templatesを使用してください - Rust の同等機能である Future/Poll モデルについては、
skills/rust/rust-async-internalsを使用してください - GDB セッションの管理については、
skills/debuggers/gdbを使用してください