ことの発端
たまに、というか稀にOSやライブラリのAPIでコールバック関数を引数とする所謂非同期関数があったりする。
例えば、こんな感じ
extern "C" int asyncfunc(int p1, int p2, void* refcon, void(*callback)(void*, int));
とりあえず、この記事で asyncfunc
は p1 と p2 を加算したものを暫く経過してからコールバック関数に渡す非同期関数とします。
さて、MacintoshのToolboxでお馴染み(古い)の refcon が出てきましたが、こいつはコールバックを呼び出す際にそのまま透過して受け取れるポインタで、一般的には何らかのオブジェクトのポインタを渡したりします。(上記の例だとコールバック関数の第1仮引数がrefconになります)
大概Cタイプの非同期関数ってこんな感じじゃないかなと思います。
で、今どきだとラムダ式使ってこんなコードを書いてみたくなりますよね。
asyncfunc(1, 2, nullptr, [](void*, int resp){
const int n = 10;
std::cout << resp * n << std::endl;
});
おっと、const int n = 10;
は外に出したいぞ。
const int n = 10;
asyncfunc(1, 2, nullptr, [=](void*, int resp){
std::cout << resp * n << std::endl;
});
残念ながら、これはコンパイルエラーになります。
error: cannot convert 'main()::<lambda(void*, int)>' to 'void (*)(void*, int)' for argument '4' to 'int asyncfunc(int, int, void*, void (*)(void*, int))'
外部変数をキャプチャしちゃうとダメなんです。
この辺、詳しい記事は他にもあるんじゃないかと思いますので割愛します.
で、仕方なく static 関数を定義してオブジェクトを引き回したりするのですが、この手の非同期関数が多くなるとコピペの嵐になるので、何とかできないものかと考えてみました。
条件的なやつ
- 非同期関数は必ずコールバック関数を呼び出す(中断するとコールバック関数を呼ばないとかナシ)
- ラムダ式のキャプチャ並びはコピーのみとする(参照にすると生存期間がややこしい)
- 非同期関数は refcon を受け取りコールバック関数にパスしてくれる
書いてみた
# include <iostream>
# include <tuple>
# include <functional>
# include <thread>
# include <sstream>
extern "C" void asyncfunc(int p1, int p2, void* refcon, void(*callback)(void*, int)) {
/* std::thread がメモリリークしますが、テストコードなので勘弁してください */
new std::thread([=](){
std::this_thread::sleep_for(std::chrono::seconds(2));
callback(refcon, p1 + p2);
});
}
template <int N, typename FUNCTION> class staticify;
template <int N, typename RESULT, typename ...ARGS>
class staticify<N, RESULT (ARGS...)>
{
public:
using F_RES = RESULT;
using F_FUNC = std::function<RESULT(ARGS...)>;
using F_TYPE = RESULT(ARGS...);
private:
struct wrap_t {
F_FUNC f;
};
F_FUNC m_func;
static F_RES sfunc(ARGS ...args){
auto params = std::tuple<ARGS...>(args...);
auto w = reinterpret_cast<wrap_t*>(std::get<N>(params));
auto f = w->f;
delete w;
return f(args...);
}
public:
staticify(F_FUNC f): m_func(f) {}
void* refcon() { return new wrap_t{ .f = m_func }; }
static F_TYPE* callback() { return &sfunc; }
};
void test(int n) {
auto st = staticify<0, void(void*, int)>([=](auto /* refcon */, auto resp){
std::stringstream ss;
ss << resp * n << ", ";
/* 本当はブロッキングしないとアカンけど勘弁してください */
std::cout << ss.str();
});
asyncfunc(1, 2, st.refcon(), st.callback());
}
int main(void) {
std::cout << "before test()" << std::endl;
test(2);
test(10);
test(3);
test(6);
std::cout << "after test()" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
}
staticify
のテンプレート第1引数はコールバック関数の第N
引数に refcon
が入ってくるという大切な指示で、間違えると爆発します。
振り返り
- もうちょいうまくやれば、C++17以降のクラステンプレートのテンプレート引数推論を使ってシンプルに書けそう。
-
wrap_t
の取り回しなんかは罪悪感しかない。。。 - テンプレートの特殊化やってるけど、これで合ってるのか? もうちょい考えるところはないのか?
- 最近はCの非同期関数使うケースなんて少ないだろうから誰得?
-
asyncfunction
そのものを良い感じに変換できるクラス作ればもっと良いかも。