関数ポインターとしてのキャプチャーを備えたC ++ラムダ


93

私はC ++ラムダとそれらの関数ポインタへの暗黙の変換で遊んでいました。私の最初の例は、それらをftw関数のコールバックとして使用していました。これは期待どおりに機能します。

#include <ftw.h>
#include <iostream>

using namespace std;

int main()
{
    auto callback = [](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        cout << fpath << endl;
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    return ret;
}

キャプチャを使用するように変更した後:

int main()
{

    vector<string> entries;

    auto callback = [&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

コンパイラエラーが発生しました:

error: cannot convert main()::<lambda(const char*, const stat*, int)>’ to __ftw_func_t {aka int (*)(const char*, const stat*, int)}’ for argument 2 to int ftw(const char*, __ftw_func_t, int)’

いくつか読んだ後。キャプチャを使用するラムダは、暗黙的に関数ポインターに変換できないことを学びました。

これの回避策はありますか?「暗黙的に」変換できないという事実は、「明示的に」変換できることを意味しますか?(私はキャストを試みましたが、成功しませんでした)。ラムダを使用していくつかのオブジェクトにエントリを追加できるように、作業例を変更するためのクリーンな方法は何ですか?


どのコンパイラを使用していますか?VS10ですか?
Ramon ZarazuaB。2011

gccバージョン4.6.1 20110801 [gcc-4_6-branchリビジョン177033](SUSE Linux)
duncan '21 / 10/21

4
通常、コールバックに状態を渡すCの方法は、コールバックへの追加の引数(通常は型void *)を介して行われます。使用しているライブラリでこの追加の引数が許可されている場合は、回避策があります。そうしないと、やりたいことをきれいに達成する方法がありません。
アレクサンドルC.

はい。ftw.hとnftw.hのAPIに欠陥があることに気づきました。fts.hを試してみる
duncan

1
すごい!/usr/include/fts.h:41:3:エラー:#error "<fts.h>は-D_FILE_OFFSET_BITS == 64と一緒に使用できません"
duncan

回答:


46

ラムダをキャプチャする場合は状態を保持する必要があるため、ラムダ単なる通常の関数ではないため、実際には単純な「回避策」はありません。関数ポインタのポイントは、単一のグローバル関数を指し、この情報には状態の余地がないということです。

最も近い回避策(本質的にステートフル性を破棄する)は、ラムダ/関数からアクセスされるあるタイプのグローバル変数を提供することです。たとえば、従来のファンクターオブジェクトを作成して、いくつかの一意の(グローバル/静的)インスタンスを参照する静的メンバー関数を与えることができます。

しかし、それはラムダをキャプチャするという目的全体を打ち負かすようなものです。


3
より明確な解決策は、関数ポインターにコンテキストパラメーターがあると想定して、ラムダをアダプター内にラップすることです。
Raymond Chen

4
@RaymondChen:ええと、関数の使用方法を自由に定義できるのであれば、そうです、それはオプションです。その場合、パラメーターをラムダ自体の引数にするだけの方が簡単です!
Kerrek SB、2011

3
@KerrekSBはグローバル変数をaに入れ、namespaceとしてマークします。これはthread_localftw同様の問題を解決するために私が選択したアプローチです。
KjellHedström14年

「関数ポインターは単一のグローバル関数を指し、この情報には状態の余地がありません。」->では、Javaなどの言語でこれを実現するにはどうすればよいのでしょうか。まあ、当然のことながら、その単一の、グローバル関数が作成されるため、実行時とは、埋め込み(というか、状態の参照、独自のコードで、それまでに)。それはある全体のポイント-そこにする必要がありませんこと、単一の、グローバル関数が、複数のグローバル関数-ラムダは実行時に使用されるたびに1つずつ。それを行うC ++には本当に何もありませんか?(私はstd :: functionがその単一の目的のために正確に作られていると思いました)
Dexter '30

1
@Dexter:errrr ..短い答えは「いいえ」、長い答えはオペレーターの過負荷を伴います。とにかく、私の主張は立っています。JavaはC ++とは異なる別の言語です。Javaにはポインター(またはオーバーロード可能な呼び出し演算子)がなく、比較がうまく機能しません。
Kerrek SB 2017

47

私はこの問題に遭遇しました。

コードはラムダキャプチャなしで正常にコンパイルされますが、ラムダキャプチャで型変換エラーが発生します。

C ++ 11でのソリューションは、使用することですstd::function(編集:関数のシグネチャを変更する必要のない別のソリューションは、この例の後に示されています)。使用することもできますboost::function(実際にはかなり高速に実行されます)。サンプルコード-コンパイル、コンパイルされるように変更gcc 4.7.1

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

int ftw(const char *fpath, std::function<int (const char *path)> callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };

  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

編集:元の関数シグネチャを変更できなかったが、ラムダを使用する必要があったレガシーコードに遭遇したとき、私はこれを再訪する必要がありました。元の関数の関数シグネチャを変更する必要がないソリューションは次のとおりです。

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// Original ftw function taking raw function pointer that cannot be modified
int ftw(const char *fpath, int(*callback)(const char *path)) {
  return callback(fpath);
}

static std::function<int(const char*path)> ftw_callback_function;

static int ftw_callback_helper(const char *path) {
  return ftw_callback_function(path);
}

// ftw overload accepting lambda function
static int ftw(const char *fpath, std::function<int(const char *path)> callback) {
  ftw_callback_function = callback;
  return ftw(fpath, ftw_callback_helper);
}

int main() {
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };
  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

72
いいえ、これは受け入れられる答えではありません。ポイントは関数ポインタの代わりftwに取るようstd::functionに変化していません...
グレゴリーパコス

この回答で提案されている2番目の解決策は、元の署名を保持することによって@ gregory-pakoszからの懸念に対処しますが、グローバルな状態を導入するため、それはまだ素晴らしいものではありません。ftwvoid * userdata引数があった場合は、@ evgeny-karpovからの回答をお勧めします。
プライドアウト

@prideoutは同意しました-私もグローバルな状態が好きではありません。残念ながら、twwのシグネチャは変更できないと想定し、ユーザーデータがvoid *でない場合、状態はどこかに保存する必要があります。サードパーティのライブラリを使用してこの問題に遭遇しました。ライブラリがコールバックをキャプチャして後でそれを使用しない限り、これは正常に機能します。その場合、グローバル変数は単にコールスタックの追加のパラメーターのように機能します。ftwの署名を変更できる場合は、void * userdataの代わりにstd :: functionを使用することをお勧めします。
ジェイウエスト

1
これは非常に複雑で便利な解決策です。@ Gregoryで「うまくいく」と伝えます。
fiorentinoing

16

元の

ラムダ関数は非常に便利で、コードを減らします。私の場合、並列プログラミングにはラムダが必要でした。ただし、キャプチャと関数ポインタが必要です。私の解決策はここにあります。ただし、キャプチャした変数のスコープには注意してください。

template<typename Tret, typename T>
Tret lambda_ptr_exec(T* v) {
    return (Tret) (*v)();
}

template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
Tfp lambda_ptr(T& v) {
    return (Tfp) lambda_ptr_exec<Tret, T>;
}

int a = 100;
auto b = [&]() { a += 1;};
void (*fp)(void*) = lambda_ptr(b);
fp(&b);

戻り値の例

int a = 100;
auto b = [&]() {return a;};
int (*fp)(void*) = lambda_ptr<int>(b);
fp(&b);

更新

改良版

関数ポインターとしてのキャプチャー付きのC ++ラムダについての最初の投稿が投稿されてからしばらく経ちました。自分や他の人にも使えるので改善しました。

標準関数Cポインターapiは、void fn(void * data)規則を使用します。デフォルトではこの規則が使用され、ラムダはvoid *引数で宣言する必要があります。

実装の改善

struct Lambda {
    template<typename Tret, typename T>
    static Tret lambda_ptr_exec(void* data) {
        return (Tret) (*(T*)fn<T>())(data);
    }

    template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
    static Tfp ptr(T& t) {
        fn<T>(&t);
        return (Tfp) lambda_ptr_exec<Tret, T>;
    }

    template<typename T>
    static void* fn(void* new_fn = nullptr) {
        static void* fn;
        if (new_fn != nullptr)
            fn = new_fn;
        return fn;
    }
};

int a = 100;
auto b = [&](void*) {return ++a;};

キャプチャ付きラムダをCポインタに変換する

void (*f1)(void*) = Lambda::ptr(b);
f1(nullptr);
printf("%d\n", a);  // 101 

この方法でも使用できます

auto f2 = Lambda::ptr(b);
f2(nullptr);
printf("%d\n", a); // 102

戻り値を使用する必要がある場合

int (*f3)(void*) = Lambda::ptr<int>(b);
printf("%d\n", f3(nullptr)); // 103

そして、データが使用される場合

auto b2 = [&](void* data) {return *(int*)(data) + a;};
int (*f4)(void*) = Lambda::ptr<int>(b2);
int data = 5;
printf("%d\n", f4(&data)); // 108

3
これは間違いなく、ラムダをCスタイルの関数ポインターに変換するために見た最も便利なソリューションです。引数として受け取る関数には、その状態を表す追加のパラメーターが必要です。Cライブラリでは、多くの場合、 "void * user"と呼ばれるため、呼び出すときに関数ポインターに渡すことができます。
コドスコープ2017

10

ローカルグローバル(静的)メソッドを使用すると、次のように実行できます。

template <class F>
auto cify_no_args(F&& f) {
  static F fn = std::forward<F>(f);
  return [] {
    return fn();
  };
}

私たちが持っていると仮定します

void some_c_func(void (*callback)());

したがって、使用法は

some_c_func(cify_no_args([&] {
  // code
}));

これは、ラムダごとに一意の署名があるため機能し、静的にすることは問題ありません。以下は、同じメソッドを使用する可変個の引数と戻り値の型を持つ汎用ラッパーです。

template <class F>
struct lambda_traits : lambda_traits<decltype(&F::operator())>
{ };

template <typename F, typename R, typename... Args>
struct lambda_traits<R(F::*)(Args...)> : lambda_traits<R(F::*)(Args...) const>
{ };

template <class F, class R, class... Args>
struct lambda_traits<R(F::*)(Args...) const> {
    using pointer = typename std::add_pointer<R(Args...)>::type;

    static pointer cify(F&& f) {
        static F fn = std::forward<F>(f);
        return [](Args... args) {
            return fn(std::forward<Args>(args)...);
        };
    }
};

template <class F>
inline lambda_traits<F>::pointer cify(F&& f) {
    return lambda_traits<F>::cify(std::forward<F>(f));
}

そして同様の用法

void some_c_func(int (*callback)(some_struct*, float));

some_c_func(cify([&](some_struct* s, float f) {
    // making use of "s" and "f"
    return 0;
}));

1
これはクロージャー(ptrを取得するとき)+ args(呼び出し時)をコピーすることに注意してください。それ以外の場合は、エレガントなソリューションです
Ivan Sanz-Carasa '21 / 08/21

ヘッダーのみのヘルパーライブラリ:gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa '21 / 08/21

1
@ IvanSanz-Carasaご指摘ありがとうございます。クロージャタイプはCopyAssignableではありませんが、ファンクタはそうです。つまり、ここでは完全転送を使用することをお勧めします。一方、argsの場合、プレーンなCではユニバーサル参照がサポートされていないため、多くのことはできませんが、少なくともラムダに値を転送できます。これにより、余分なコピーが保存される場合があります。コードを編集しました。
Vladimir Talybin、2018年

@RiaDはい。ラムダは静的インスタンスなので、代わりに、たとえばforループで=使用する代わりに、参照でキャプチャする必要があり&iます。
Vladimir Talybin

5

へへ-かなり古い質問ですが、それでも...

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// We dont try to outsmart the compiler...
template<typename T>
int ftw(const char *fpath, T callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  // ... now the @ftw can accept lambda
  int ret = ftw("/etc", [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  });

  // ... and function object too 
  struct _ {
    static int lambda(vector<string>& entries, const char* fpath) {
      entries.push_back(fpath);
      return 0;
    }
  };
  ret = ftw("/tmp", bind(_::lambda, ref(entries), placeholders::_1));

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

0

キャプチャするラムダを関数ポインターに変換するハッキーな方法がありますが、それを使用するときは注意する必要があります。

/codereview/79612/c-ifying-a-capturing-lambda

コードは次のようになります(警告:脳のコンパイル)。

int main()
{

    vector<string> entries;

    auto const callback = cify<int(*)(const char *, const struct stat*,
        int)>([&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    });

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

0

私の解決策は、関数ポインターを使用して静的ラムダを参照することだけです。

typedef int (* MYPROC)(int);

void fun(MYPROC m)
{
    cout << m(100) << endl;
}

template<class T>
void fun2(T f)
{
    cout << f(100) << endl;
}

void useLambdaAsFunPtr()
{
    int p = 7;
    auto f = [p](int a)->int {return a * p; };

    //fun(f);//error
    fun2(f);
}

void useLambdaAsFunPtr2()
{
    int p = 7;
    static auto f = [p](int a)->int {return a * p; };
    MYPROC ff = [](int i)->int { return f(i); };
    //here, it works!
    fun(ff);
}

void test()
{
    useLambdaAsFunPtr2();
}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.