ことの発端
Cでよくある非同期関数
extern "C" int asyncfunc(int p1, int p2, void* refcon, void(*callback)(void*, int));
asyncfunc(1, 2, nullptr, [](void*, int resp){
const int n = 10;
std::cout << resp * n << std::endl;
});
const int n = 10;
asyncfunc(1, 2, nullptr, [=](void*, int resp){
std::cout << resp * n << std::endl;
});
ここに、ラムダ導入子有りのラムダ式を使うとエラーになるという問題を何とかできないかという話です。
これは、前回投稿させていただいた「Cでよくある非同期関数のコールバック関数でラムダ導入子有りのラムダ式を使えるようにしたい」の続きになります。
条件的なやつ
- 非同期関数は必ずコールバック関数を呼び出す(中断するとコールバック関数を呼ばないとかナシ)
- ラムダ式のキャプチャ並びはコピーのみとする(参照にすると生存期間がややこしい)
- 非同期関数は refcon を受け取りコールバック関数にパスしてくれる
- 非同期関数の最終引数はコールバック関数1
- 非同期関数の最終引数の1個前は refcon1
書いてみた
で、前回投稿させていただいた staticify
を使って無い知恵絞ってみた感じです。
# 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_ARGS = std::tuple<ARGS...>;
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; }
};
template <int N, typename FUNCTION> class lambda_enabler;
template <int N, typename RESULT, typename ...ARGS>
class lambda_enabler<N, RESULT (ARGS...)>
{
using F_RES = RESULT;
using F_ARGS = std::tuple<ARGS...>;
using F_FUNC = std::function<RESULT(ARGS...)>;
using CB_F_TYPE = typename std::tuple_element<std::tuple_size<F_ARGS>::value - 1, F_ARGS>::type;
using STATICIFY = staticify<N, CB_F_TYPE>;
using CB_FUNC = std::function<typename STATICIFY::F_TYPE>;
F_FUNC m_target;
public:
lambda_enabler(F_FUNC target) : m_target(target) {}
template <typename ...LARGS> auto prepare(LARGS ...largs) {
struct p {
const std::function <F_RES(void*, CB_F_TYPE)> f;
void call(CB_FUNC cb) {
STATICIFY x(cb);
f(x.refcon(), x.callback());
}
};
return p{ .f = std::bind(m_target, largs..., std::placeholders::_1, std::placeholders::_2)};
}
};
int main(void) {
auto x = lambda_enabler<0, decltype(asyncfunc)>(asyncfunc);
for(auto i = 0; i < 10; i++){
x.prepare(i + 1, i + 3).call([=](auto, auto n){
std::stringstream ss;
ss << n * i << std::endl;
std::cout << ss.str();
});
}
std::this_thread::sleep_for(std::chrono::seconds(5));
}
lambda_enable
という名前が微妙なのは勘弁していただいて。。。
使い方
例えば asyncfunc
で、やりたいことが、こんな感じだとする。
int i = 100;
asyncfunc(1, 2, nullptr, [=](auto, auto n){
std::cout << i + n;
});
で、lambda_enabler
をこんな感じで生成します。
auto x = lambda_enabler<0, decltype(asyncfunc)>(asyncfunc);
テンプレートパラメータの先頭にある0
(非型テンプレートパラメータ)はコールバック関数で refcon が何個目のパラメータに入るかという情報になります。
以降は、こんな感じで使えます。
int i = 100;
x.prepare(1, 2).call([=](/* void* */ auto, /* int */ auto n){
std::cout << i + n;
});
ラムダ式で記述しているコールバック関数の第1引数はrefconが渡ってきますが、決して触らないでください。(苦笑)
prepare()
でrefcon
とコールバック関数
以外を指定して、そいつからの call()
で実際に asyncfunc
を呼び出すみたいな感じです。
振り返り
- 何となく良さそうな気がしないこともない。
- 非同期関数のrefconを渡す場所が最終引数の1個前に固定されるのが残念。(でも、こんな引数のパターンが多いんじゃない?)
- パラメータパックの最後の2つを差し替えたいんだけど方法が思いつかなかったので
prepare
の型が不定になってしまう。(勿論、型が違えばコンパイル時にエラーになりますが)2 - 稀に(よくある?)、
asyncfunc
の返却値を確認しなきゃならない場合があったりするが、それは無視する方向で良いケースと悪いケースがあるので、その辺の機能を入れても良いが複雑になるなぁ〜 - ラムダ式にrefcon無くしたいかも
github で公開しました(2021/03/06 追記)
要素機能的なものを組み合わせ、コードを整理して github に公開してみました。
ついでに、C++17 以降であれば、operator()
をオーバーロードして、こんな感じで呼び出せるようにしてみました。
asyncfn({1, 2}, [=]((/* void* */ auto, /* int */ auto n){
std::cout << i + n;
});
これで、かなり快適になりました。
ただ、C++17使えばlambda_enabler
を実体化する際に decltype(asyncfunc)
を推論でいけるかと思ったのですが、ダメでした。。。
まだ修業が足りない。。。
実は(2021/03/06 追記)
node.js の util.promisify
を知り、C++ でも出来ないかな~というのが、本当の「ことの発端」でした。
で、これまでの積み重ねでようやく非同期関数を reactive extension の observable
に変換する observablify
を作ることができました。3
auto rxfn = observablify<0, decltype(asyncfunc)>(asyncfunc);
rxcpp::observable<>::range(0, 9, rxcpp::observe_on_new_thread())
.flat_map([=](auto n){
return rxfn.rx(n + 1, n + 3)
.map([=](auto r){
return std::get<1>(r) * n;
}).as_dynamic();
}).as_dynamic()
.subscribe([](auto x){
std::cout << x << std::endl;
});
じゃ~、rxjs
の observable
もと思ったけど、 util.promisify
があるならObservable.from
でしまいですね。(苦笑)
メタプログラミングって、ウチらみたいな業務アプリ作っているとなかなか作る機会がないけど、ひょんなキッカケで体験することができて色々勉強になりました。
-
トライしてみました → 可変引数テンプレートのパラメータパックを任意の場所で分解して std::tuple にしたい ↩
-
実は先日
util.promisify
をC++でやる的な投稿したのですが、非同期関数のコールバック関数を関数ポインタではなくstd::function
で受けてしまい、普通にラムダ式が使えてしまえるということに気づき投稿は削除、そして、そこからこんな展開になるとは。。。 ↩