C++20のコルーチン for アプリケーション

C++20でコルーチンが追加された。
この記事ではアプリケーションユーザ(notライブラリ読者/開発者)向けにC++20のコルーチンについて説明する

## C++20のコルーチンの特徴
C++20のコルーチンのめぼしい特徴に以下がある

- 細かな制御/拡張ポイント
  - 例えば以下のような制御/拡張ポイントがある
     - コルーチンを作成したとき、一番最初の中断点まで進めるか
     - 例外をどう取り扱うか
     - このコルーチンの中断時の挙動はどうするか
- コールスタックを保存しないことによる高速なコルーチン(stacklessコルーチン)
  - コンパイル時に確保すべきメモリサイズが確定できる & オブジェクト生成時に一括で確保する。  
したがってコルーチン中断時にスタックに積まれたデータなどを動的に退避しなくてよくなる(vs stackfulコルーチン)
  - 欠点は「awaitを呼ぶ(コルーチンではない)関数」のようなことができなくなる
     - ただ、コルーチンからコルーチンを呼ぶことはできるので実用上そこまで問題にはならない

## C++のコルーチンに無いもの

- 「細かな制御/拡張ポイント」のpreset、つまりライブラリ的な部分がほぼ無

:innocent: 

でもまぁ、無い部分は自分で書けばいいので、大丈夫だろう。やっていこう

幸い自分で書かなくても cppcoroという便利っぽいライブラリがある
読者がもしコルーチンを使う状況になったとき、そのとき使うコルーチンライブラリがcppcoroなのか、何らかのフレームワークの一部として提供されたものか、それとも標準ライブラリに入ったものなのか、私には知ることができない
しかしcppcoroを使ったサンプルは理解の助けになるだろう

そんなわけでコルーチンやっていこう

## 文法

ものすごい雑に言うと関数の実装で`co_{await,yield,return}`を使って戻り値の型に使いたいコルーチン型を指定するとコルーチンになる

サンプル

```cpp
// コルーチン定義
cppcoro::generator<const std::uint64_t> fibonacci(){
  std::uint64_t a = 0, b = 1;
  while (true){
    co_yield b;
    auto tmp = a;
    a = b;
    b += tmp;
  }
}

//コルーチンを使う
int main(){
    auto g = fibonacci();
    for (auto i : g){
        if (i > 1'000'000) break;
        std::cout << i << std::endl;
    }
}
```

### コルーチン(を返す関数)の作り方

1. コルーチンで使いたい型(以下 MyCoroutineType)を選ぶ
2. 関数の戻り値にMyCoroutineTypeを指定するとその関数はコルーチンになる（※）
3. MyCoroutineTypeのドキュメントを見ながら、コルーチンの実装を書く

となる

```cpp
// 1. 今回はcppcoroのgeneratorを使って無限フィボナッチ数列を生成するコルーチンを生成してみようと思う
// https://github.com/lewissbaker/cppcoro/tree/69e575480afe97fd342a44f580ba8ff3b4a9a58a#generatort

// 2. 関数の戻り値に使いたい `cppcoro::generator<const std::uint64_t>` を指定する
cppcoro::generator<const std::uint64_t> fibonacci()
// 3. 実装する 
// generatorは pythonにある奴と大体同じ、co_yield に値を渡すとそれをコルーチンの外に渡すやつである
{
  std::uint64_t a = 0, b = 1;
  while (true)
  {
    co_yield b; // b の値が外に渡され、中断される
    auto tmp = a;
    a = b;
    b += tmp;
  }
}
```

(※) 厳密にはこれは2つの点で間違いである。

1. 「関数内でco_yieldなどコルーチンのキーワードを使うとコルーチンになる」が正しい
  - コルーチンのキーワードを関数内で使わずにMyCoroutineTypeを指定すると、それはただのMyCoroutineTypeを返す関数になる
  - 逆にコルーチンのキーワードを関数内で使いながら戻り値にコルーチン型を指定しなかった場合、コンパイルエラーとなる
2. std::coroutine_traitsを特殊化することで、戻り値型だけではなく引数型もコルーチンのルールの決定に加味できるようになる。また、本来コルーチンとして使えない型もコルーチンとして使えるように妥当な実装を与えることができる。面倒なのでここでは紹介しない


### コルーチンの使い方
1. 「コルーチンの作り方」で作った関数を実行するとMyCoroutineTypeのオブジェクトが返る
2. 使いたい型のドキュメントを見ながら使う

```cpp
// 1. generator<const std::uint64_t>型が返る
auto g = fibonacci();

// 2. ドキュメントを読んで便利に使おう
// generatorはrangeのように使える
for (auto i : g)
{
    if (i > 1'000'000) break;
    std::cout << i << std::endl;
}
```

### co_await,co_yield,co_return 詳細

コルーチンでは新たに以下の3つのキーワードが登場する

- co_await
- co_yield
- co_return

これらの挙動は使うコルーチン型や引数などによっていろいろカスタマイズ可能であり、「co_awaitはこう動く」みたいなことはここでは言いづらい
「正確な挙動はライブラリのドキュメントを読もう」という感じなのだが
これらについて一般的に言えることを述べる



#### co_await式

```cpp
bytesRead = co_await sock.recv(buffer.get(), bufferSize);
```

コルーチンを中断するやつ。呼び出されるとコルーチンを中断する

一般的にAwaiterと呼ばれる型、もしくはコルーチンを引数にとる
co_awaitの引数の型等によっては再開時に演算子から返り値が返る

#### co_yield式

yield である。 一般的に呼び出されると外にその値を渡し、コルーチンを中断する
使用するコルーチン等によっては再開時に演算子から返り値が返る

#### co_return 文

```cpp
co_return expr;
```

```cpp
co_return;
```

コルーチン版return。直感的にはreturnと同じ挙動をする
つまり、引数を外に返し、コルーチンを終了する

### cppcoroのgeneratorとtaskの使い方

最後に cppcoroのコルーチン型を2つ紹介する
この記事の読者がcppcoroを使うかは分からない
しかし具体的なサンプルを見ることでコルーチンの各々がどのように使われるかについて、理解の助けになると思われる

#### generator

generatorは値を生成する関数の結果をイテレータの組(range)にして返してくれるやつである
pythonやluaにあるgeneratorと同じものだ
https://github.com/lewissbaker/cppcoro/tree/69e575480afe97fd342a44f580ba8ff3b4a9a58a#generatort

```cpp

cppcoro::generator<int> range(){
    int n = 5000;
    while(1){
        if(n==0) co_return; // n==0になったらコルーチンを脱出する。co_return文は値をとらない
        co_yield n/3;  // 値を渡し
        std::cout<<"ho!!!"<<std::endl;  // 遅延評価されてることを確認するために副作用を起こしてみる
        n/=3;
    }
}

int main(){
    for(auto x: range()){
        std::cout<<"#"<<x<<std::end;
    }
}

```

cppcoro::generatorでは主にco_yieldをつかう
co_awaitは使わない(使えない)
また、co_returnは値をとらない

動くサンプル: https://wandbox.org/permlink/MYa1qv2grlgsO36i

#### task
taskはco_awaitを使って非同期処理を見た目同期的にやるためのコルーチンである
https://github.com/lewissbaker/cppcoro/tree/69e575480afe97fd342a44f580ba8ff3b4a9a58a#taskt

```c++
cppcoro::task<int> count_lines(std::string path)
{
  auto file = co_await cppcoro::read_only_file::open(path);  // 開くまでコルーチンは再開できない

  int lineCount = 0;

  char buffer[1024];
  size_t bytesRead;
  std::uint64_t offset = 0;
  do
  {
    bytesRead = co_await file.read(offset, buffer, sizeof(buffer));  // 読み終わるまでコルーチンは再開できない
    lineCount += std::count(buffer, buffer + bytesRead, '\n');
    offset += bytesRead;
  } while (bytesRead > 0);

  co_return lineCount; // task<T>ではT型の値を返せる(T=voidを指定すれば返さないこともできる)
}

// taskをcppcoro::sync_wait()等に渡すといい感じに再開処理を実行する
```

cppcoro::taskでは主にco_awaitを使う
co_awaitは 前述の通り Awaiter と呼ばれる型をとる。Awaiterは次を制御できる

- このコルーチンは中断すべきか(中断しないことも可能)
- 中断したとしてどのような副作用(等)をおこすか(例えばメッセージ投げたりとかsleepしたりとか)
- co_await演算子の戻り値に何を返すか

また、task<>自身もAwaiter(への変換が可能)であるため、co_awaitに渡すことができる(というかそれを主にやっていく)

Awaiterはcppcoroにいくつか定義されている(socketとか)
それらについて説明を始めると本当に大変になってしまう+コルーチンそのものの説明から大きく脱線してしまうので、興味がある方はcppcoroのドキュメントを読んでほしい



## 参考

- https://www.modernescpp.com/index.php/c-20-coroutines-with-cppcoro
- https://github.com/lewissbaker/cppcoro
- https://timsong-cpp.github.io/cppwp/n4861/coroutine.syn#:%3ccoroutine%3e
- https://timsong-cpp.github.io/cppwp/n4861/dcl.fct.def.coroutine
- https://cpprefjp.github.io/lang/cpp20/coroutines.html