hamigaki.png

前のページ 上に戻る ホーム 次のページ

チュートリアル

コルーチン
ジェネレータ
コールバック関数

コルーチン

コルーチンとは、処理を中断したり、再開したりすることのできるサブルーチンの変種である。

Hamigaki.Coroutineはコルーチンを作成するためのクラステンプレートcoroutineを提供している。ここではcoroutineを用いて掛け算九九の答えを表示するプログラムを考える。

#include <hamigaki/coroutine/coroutine.hpp>

これはcoroutineを使用するために必要である。

namespace coro = hamigaki::coroutines;

typedef coro::coroutine<int()> coroutine_type;

coroutineは名前空間hamigaki::coroutinesで定義される。唯一のテンプレート引数はコルーチンの型を指定するためのものである。ここではint型の戻り値を持ち、引数を持たないコルーチンを使用する。

int kuku_body(coroutine_type::self& self)
{
    for (int i = 1; i <= 9; ++i)
        for (int j = 1; j <= 9; ++j)
            self.yield(i*j);

    self.exit();
}

関数kuku_body()はコルーチンの本体である。この関数の型はcoroutineのテンプレート引数に指定したものにcoroutine_type::self&型の引数を追加したものになる。この追加された引数はコルーチンの制御を行うために使用される。

メンバ関数yield()はコルーチンの呼び出し側に制御を戻し、引数をコルーチンの計算結果として返す関数である。

また、メンバ関数exit()はコルーチンの実行を中断し、呼び出し側に制御を戻す関数である。

#include <iostream>

int main()
{
    try
    {
        coroutine_type kuku(kuku_body);
        while (true)
            std::cout << kuku() << std::endl;
    }
    catch (const coro::coroutine_exited&)
    {
    }
}

coroutineのコンストラクタの引数にはコルーチンの本体を指定する。

作成されたcoroutineは関数オブジェクトとして機能する。コルーチンを呼び出すと、制御がコルーチン本体へと移る。コルーチン側はyield()が呼ばれるか、returnされるまで実行され、その結果が呼び出し側に返される。再びコルーチンを呼び出すと、前回yield()を呼んだ箇所から実行が再開される。

メンバ関数exit()を呼ぶか、コルーチンからreturnすると、コルーチンは終了する。終了したコルーチンを呼び出すと、例外coroutine_exitedが発生する。コルーチンはその性質上、呼び出すまで終了したかどうか分からないので、この例外を捕捉することで終了を検知する。

別の方法として、コルーチンの呼び出し時にstd::nothrowを引数として指定することもできる。この場合、戻り値がboost::optionalになり、終了を検知した場合は空となる。

#include <iostream>

int main()
{
    coroutine_type kuku(kuku_body);
    while (boost::optional<int> next = kuku(std::nothrow))
        std::cout << *next << std::endl;
}

完全なプログラムは以下のようになる。

#include <hamigaki/coroutine/coroutine.hpp>

namespace coro = hamigaki::coroutines;

typedef coro::coroutine<int()> coroutine_type;

int kuku_body(coroutine_type::self& self)
{
    for (int i = 1; i <= 9; ++i)
        for (int j = 1; j <= 9; ++j)
            self.yield(i*j);

    self.exit();
}


#include <iostream>

int main()
{
    try
    {
        coroutine_type kuku(kuku_body);
        while (true)
            std::cout << kuku() << std::endl;
    }
    catch (const coro::coroutine_exited&)
    {
    }
}

ジェネレータ

オブジェクトの列を生成し、一要素ずつ順に返すサブルーチンをジェネレータと呼ぶ。

Hamigaki.Coroutineはジェネレータを作成するためにクラステンプレートgeneratorを提供しており、これはコルーチンを用いて実装されている。今度は、先程の九九のプログラムをジェネレータを用いて書き直すことを考える。

#include <hamigaki/coroutine/generator.hpp>

これはgeneratorを使用するために必要である。

namespace coro = hamigaki::coroutines;

typedef coro::generator<int> generator_type;

generatorのテンプレート引数はコルーチンの型ではなく、戻り値の型である。

int kuku_body(generator_type::self& self)
{
    for (int i = 1; i <= 9; ++i)
        for (int j = 1; j <= 9; ++j)
            self.yield(i*j);

    self.exit();
}

関数kuku_body()はコルーチンの本体である。これはcoroutinegeneratorに置き換わっただけである。

メインプログラムは次のように書ける。

#include <algorithm>
#include <iostream>
#include <iterator>

int main()
{
    std::copy(
        generator_type(kuku_body), generator_type(),
        std::ostream_iterator<int>(std::cout, "\n")
    );
}

generatorのコンストラクタにコルーチンの本体を渡すことでジェネレータを作成する。省略時初期化されたgeneratorは終端を表すために利用される。

generatorは入力反復子の要件を満たすので、入力反復子を受け取るアルゴリズムに渡すことができる。

コールバック関数

C言語でオブジェクトを列挙する場合、主に次のインタフェースが利用される。

  • 列挙用のハンドルを通して、順次問い合わせる(プル型)
  • 各要素に対し、コールバック関数を呼ぶ(プッシュ型)

前者の例としては、POSIXのopendir()/readdir()/closedir()を用いたディレクトリの走査が挙げられる。このインタフェースは列挙を進めることも中断することも自由に行うことができるため、反復子で表現するのが容易である。

一方、後者のインタフェースはMicrosoft Windowsで頻繁に用いられるものである。このインタフェースは、列挙の主体がユーザー側にないため、反復子で表現することは困難である。しかし、Hamigaki.Coroutineを用いることでこれが可能となる。

例として、ウィンドウハンドル(HWND)を列挙するEnumWindows()関数を反復子で表現することを考える。

#include <hamigaki/coroutine/generator.hpp>
#include <algorithm>
#include <iostream>
#include <windows.h>

namespace coro = hamigaki::coroutines;

typedef coro::generator<HWND> enum_windows_iterator;

BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam)
{
    try
    {
        enum_windows_iterator::self& self =
            *reinterpret_cast<enum_windows_iterator::self*>(lParam);

        self.yield(hwnd);
        return TRUE;
    }
    catch (...)
    {
    }
    return FALSE;
}

HWND enum_windows_body(enum_windows_iterator::self& self)
{
    ::EnumWindows(&enum_windows_callback, reinterpret_cast<LPARAM>(&self));
    self.exit();
}

enum_windows_body()がコルーチンの本体である。EnumWindows()はこの中で呼び出す。EnumWindows()には各ウィンドウハンドル毎に呼び出すコールバック関数とそれに渡すデータを引数として渡すようになっている。コールバック関数enum_windows_callback()selfを必要とするので、ここではそのポインタを渡している。

enum_windows_callback()では、yield()でウィンドウハンドルを返し、制御を呼び出し側に戻す。

反復子が進められると処理がコルーチン側に戻り、コールバック関数からはTRUEが返される。これは列挙の継続を意味している。

反復子を末尾まで進めることなく破棄した場合は、yield()が例外exit_exceptionを送出するので、これを捕捉した後にFALSEを返す。これにより列挙も中断される。

enum_windows_iteratorは入力反復子なので、次のように利用できる。

void print_hwnd(HWND hwnd)
{
    std::cout << static_cast<void*>(hwnd) << std::endl;
}

int main()
{
    std::for_each(
        enum_windows_iterator(enum_windows_body), enum_windows_iterator(),
        print_hwnd
    );
}
製作著作 © 2006-2008 Takeshi Mouri

前のページ 上に戻る ホーム 次のページ