1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Cでよくある非同期関数のコールバック関数でラムダ導入子有りのラムダ式を使えるようにしたい(続き)

Last updated at Posted at 2021-03-04

ことの発端

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;
});

じゃ~、rxjsobservable もと思ったけど、 util.promisifyがあるならObservable.fromでしまいですね。(苦笑)

メタプログラミングって、ウチらみたいな業務アプリ作っているとなかなか作る機会がないけど、ひょんなキッカケで体験することができて色々勉強になりました。

  1. 今回追加した条件になります。 2

  2. トライしてみました → 可変引数テンプレートのパラメータパックを任意の場所で分解して std::tuple にしたい

  3. 実は先日util.promisifyをC++でやる的な投稿したのですが、非同期関数のコールバック関数を関数ポインタではなくstd::function で受けてしまい、普通にラムダ式が使えてしまえるということに気づき投稿は削除、そして、そこからこんな展開になるとは。。。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?