1
2

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

ことの発端

たまに、というか稀に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 そのものを良い感じに変換できるクラス作ればもっと良いかも。
1
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?