1. サイトトップ
  2. ブログ
  3. C++
  4. 【C++】C++のコルーチンを気軽に試してみる

【C++】C++のコルーチンを気軽に試してみる

はじめに

こんにちは。
制作部プログラマの青柳です。
C++20でコルーチンが入ると聞きましたので
今回は気軽に調べて、試してみました。

コルーチンとは

Unityを使われている方ですとなじみ深いかもしれませんが関数の実行の一時中断と再開ができるというのが大きな特徴です。
C++ではスタックレス方式で実装されたそうです。(C#はスタックフル方式のようです)
スタックレス、スタックフルの違いはさて置いてCppReferenceを見てみましょう。

https://ja.cppreference.com/w/cpp/language/coroutines
ふむふむ、わからん、なんも。

コルーチンなにもわかりません

・とりあえずサンプルを写経してみました

わからないのでサンプルを探してみました。
今回参考にさせて頂いたサイトは以下になります。
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2017/october/c-from-algorithms-to-coroutines-in-c
https://www.slideshare.net/yohhoy/20c20
https://qiita.com/tan-y/items/ae54153ec3eb42f80638
https://qiita.com/yohhoy/items/aeb3c01d02d0f640c067

以下が今回のソースになります。
環境はVS2019 コマンドラインオプションに
/await
を追加してください。

// MyCoroutineTest.cpp : このファイルには 'main' 関数が含まれています。プログラム実行の開始と終了がそこで行われます。
//

#include <iostream>

#include <experimental/coroutine>

using std::experimental::coroutine_handle;
using std::experimental::suspend_always;
using std::experimental::suspend_never;

struct generator
{
	struct promise_type
	{
		int value_;
		//getRetrunObjは必ず生成
		generator get_return_object()
		{
			std::cout << "get_return_object呼び出し" << std::endl;
			return generator(*this);
		}

		//初めにResume状態にする、neverはすぐに次に進む
		suspend_always initial_suspend()
		{
			std::cout << "initial_suspend呼び出し" << std::endl;
			return {};
		}

		//終わったらResume状態、終了後自動破棄しない
		suspend_always final_suspend()
		{
			std::cout << "final_suspend呼び出し" << std::endl;
			return {};
		}

		//co_yieldのたびに呼ばれる、値をコピーするためのメソッド
		suspend_always yield_value(int value)
		{
			std::cout << "yield_value呼び出し 値 " << value << " をコピー" << std::endl;
			
			value_ = value;
			return {};
		}

		void return_void()
		{
			std::cout << "co_voidで終わった呼び出し" << std::endl;
		}

		//reeturn_voidと片方のみ
		/*int return_value(int value)
		{
			std::cout << "co_return呼び出し" << std::endl;
			return value;
		}*/

		void unhandled_exception()
		{
			std::cout << "call unhandled_exception" << std::endl;
			throw;
		}


	};
	
	generator() = default;
	generator(generator const&) = delete;//copy禁止
	generator& operator=(generator const&) = delete;

	//Move
	generator(generator&& other) noexcept : handle(other.handle)
	{
		other.handle = nullptr;
	}

	generator& operator=(generator&& other) noexcept
	{
		if (this != &other)
		{
			handle = other.handle;
			other.handle = nullptr;
		}

		return *this;
	}

	~generator()
	{
		if (handle != nullptr)//generatorはHandleを消去する
		{
			handle.destroy();
		}
	}

	//Main内でhandle.resume()を呼ぶためのメソッドを定義
	//moveNext風
	bool move_next()
	{
		std::cout << std::endl;
		std::cout << "generator move_next coroutine_handle Resume呼び出し" << std::endl;
		handle.resume();

		return !handle.done();
	}

	int currentValue()
	{
		return handle.promise().value_;
	}

private:
	explicit generator(promise_type& p)
		: handle(coroutine_handle<promise_type>::from_promise(p)) {}

	coroutine_handle<promise_type> handle;

};

generator iota(int end)
{
	for (int index = 0; index < end; ++index)
	{
		std::cout << "アプリ側 co_yield 呼び出し 値 " << index << std::endl;
		co_yield index;
	}

	//co_return -1;
	//return_valueのときはco_return
}

int main()
{
    
	auto g = iota(10);

	std::cout << "MoveNext風コルーチン実行" << std::endl;
	
		
	while (g.move_next())
	{
		int value = g.currentValue();
		std::cout << "GetCurrentでの値 " << value << std::endl;
	}
		
	
}

C++のコルーチンはコンパイル時に展開される形のようです。
co_yield, co_returnキーワードのある関数をコンパイル時に展開し
中断しているように値を保存して、再開できる形でまた実行されます。

つまり今回の形ですと
generator classのcoroutin_handleがresume()を呼ぶたびに
co_yieldが次に進みます。
co_yieldが進む前に
promise_type::yield_value
が実行され値がコピー(状態が保存)されます。
そうして最後まで呼び出し側の処理が完了すると
co_returnが呼ばれる場合は
promise_type::return_valueが呼ばれます。
co_returnがない場合は
promise_type::return_voidが呼ばれます。

呼ばれますと書いていますが、そういう風になるように展開される、という事のようです。

しかしこれは、思ったのと違う感じが、、、。
参考にさせて頂いたスライドにもありましたが
非同期IO、タスク、エラー伝搬などはライブラリ実装者が頑張って実装するという感じがしますね。
コルーチンの簡易さは時々ほしくなることがあるのでこれからのC++の進化に期待します。

、、、と共に家庭用ゲーム機向けの環境だと暫く触れることはなさそうだなという気もしています

ちなみに実行結果は以下になります。

initial_suspend呼び出し
get_return_object呼び出し
MoveNext風コルーチン実行

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 0
yield_value呼び出し 値 0 をコピー
GetCurrentでの値 0

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 1
yield_value呼び出し 値 1 をコピー
GetCurrentでの値 1

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 2
yield_value呼び出し 値 2 をコピー
GetCurrentでの値 2

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 3
yield_value呼び出し 値 3 をコピー
GetCurrentでの値 3

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 4
yield_value呼び出し 値 4 をコピー
GetCurrentでの値 4

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 5
yield_value呼び出し 値 5 をコピー
GetCurrentでの値 5

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 6
yield_value呼び出し 値 6 をコピー
GetCurrentでの値 6

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 7
yield_value呼び出し 値 7 をコピー
GetCurrentでの値 7

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 8
yield_value呼び出し 値 8 をコピー
GetCurrentでの値 8

generator move_next coroutine_handle Resume呼び出し
アプリ側 co_yield 呼び出し 値 9
yield_value呼び出し 値 9 をコピー
GetCurrentでの値 9

generator move_next coroutine_handle Resume呼び出し
co_voidで終わった呼び出し
final_suspend呼び出し

おわりに

あまり突っ込んだ内容になりませんでしたが
知らなかった事を調べられていい機会になりました。
もう少し突っ込んで調べつつC++のコミュニティや文献に触れる機会を増やしたい思います。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。