C ++ 11の再帰的ラムダ関数


143

C ++ 11は初めてです。次の再帰的なラムダ関数を書いていますが、コンパイルできません。

sum.cpp

#include <iostream>
#include <functional>

auto term = [](int a)->int {
  return a*a;
};

auto next = [](int a)->int {
  return ++a;
};

auto sum = [term,next,&sum](int a, int b)mutable ->int {
  if(a>b)
    return 0;
  else
    return term(a) + sum(next(a),b);
};

int main(){
  std::cout<<sum(1,10)<<std::endl;
  return 0;
}

コンパイルエラー:

vimal @ linux-718q:〜/ Study / 09C ++ / c ++ 0x / lambda> g ++ -std = c ++ 0x sum.cpp

sum.cpp:ラムダ関数内:sum.cpp:18:36:エラー: ' ((<lambda(int, int)>*)this)-><lambda(int, int)>::sum'は関数として使用できません

gccバージョン

gccバージョン4.5.0 20091231(試験的)(GCC)

しかし、私が次のように宣言を変更した場合sum()、それは機能します:

std::function<int(int,int)> sum = [term,next,&sum](int a, int b)->int {
   if(a>b)
     return 0;
   else
     return term(a) + sum(next(a),b);
};

誰かがこれに光を当ててもらえますか?


これは静的宣言と暗黙的動的宣言のどちらですか?
Hamish Grubijan、2010年

3
mutableそこではキーワードは何ですか?
乾杯とhth。-Alf

自動保存期間以外の変数のキャプチャは許可されていません。次のようにしてください:chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow

ちょうどFYI、2番目のコードでは、あなたのラムダが冗長すぎるスニペットこの変更を検討してください:std::function<int(int,int)> sum = [&](int a, int b) {
armanali

回答:


189

自動バージョンと完全に指定されたタイプバージョンの違いについて考えてください。自動キーワード、それがで初期化されます何から推論、その型が、何をしているが、その種類は(この場合には、ラムダクロージャは、それが捕捉していますタイプを知っている必要があります)であるかを知る必要があるとそれを初期化します。鶏と卵の問題。

一方、完全に指定された関数オブジェクトの型は、それに割り当てられているものについて何も「知る」必要がないため、ラムダのクロージャは、キャプチャする型についても完全に通知されます。

このコードのわずかな変更を検討してください。

std::function<int(int,int)> sum;
sum = [term,next,&sum](int a, int b)->int {
if(a>b)
    return 0;
else
    return term(a) + sum(next(a),b);
};

明らかに、これはautoでは機能しません。再帰的なラムダ関数は完全に適切に機能します(少なくとも、私が経験したMSVCでは機能します)。型推論と実際に互換性がないというだけのことです。


3
私はこれに同意しません。ラムダの型は関数本体に入るとすぐにわかります。それまでに推定されるべきではない理由はありません。
子犬'23年

16
@DeadMGですが、仕様autoでは初期化子の変数を参照することを禁止しています。初期化子が処理されているとき、自動変数の型はまだ不明です。
ヨハネスシャウブ-litb

1
なぜこれが「回答」としてマークされていないのか、そしてPythonは「回答」として分類されているのだろうか?
アジェイ2013年

1
@Puppy:ただし、暗黙のキャプチャの場合、効率のために参照される変数のみが実際にキャプチャされるため、本体を解析する必要があります。
kec

sum以外の有効な解釈はありますstd::function<int(int, int)>か、それともC ++仕様がそれを推測するために邪魔になっていないのですか?
Mateen Ulhaq 2018

79

コツは、キャプチャーではなく、ラムダ実装をパラメーターとしてそれ自体にフィードすることです。

const auto sum = [term,next](int a, int b) {
  auto sum_impl=[term,next](int a,int b,auto& sum_ref) mutable {
    if(a>b){
      return 0;
    }
    return term(a) + sum_ref(next(a),b,sum_ref);
  };
  return sum_impl(a,b,sum_impl);
};

コンピュータサイエンスのすべての問題は、別のレベルの間接参照で解決できます。私は最初にこの簡単なトリックをhttp://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/で見つけました

それはない質問はC ++ 11の上にある間、C ++ 14を必要としますが、ほとんどのおそらく興味深いです。

経由して行くことstd::functionも可能ですが、でき遅くコードにつながります。しかしいつもではない。std :: function vsテンプレートに対する答えを見てください


これはC ++の特殊性だけではなく、ラムダ計算の数学に直接対応しています。ウィキペディアから:

Lambda calculus cannot express this as directly as some other notations:
all functions are anonymous in lambda calculus, so we can't refer to a
value which is yet to be defined, inside the lambda term defining that
same value. However, recursion can still be achieved by arranging for a
lambda expression to receive itself as its argument value

3
これは明示的にを使用するよりもはるかに悪いようfunction<>です。誰もがそれを好む理由がわかりません。編集:それは明らかに高速です。
Timmmm 2018

17
これは、3つの理由でstd :: functionよりも優れています。これは、型の消去やメモリ割り当てを必要とせず、constexprであり、自動(テンプレート)パラメータ/戻り値の型で適切に機能します
Ivan Sanz-Carasa

3
おそらくこのソリューションには、std :: function参照がスコープ外に出ることなくコピーできるという利点もあります。
Uri Granta、

3
うーん、試してみると、GCC 8.1(linux)は不平を言いました:error: use of ‘[...]’ before deduction of ‘auto’–明示的に戻り値の型を指定する必要がありました(一方、変更可能な必要はありませんでした)。
アコンカグア

@AconcaguaはXcode10でも同じで、C ++標準を17に設定しました
IceFire

39

C ++ 14を使用すると、std::function数行のコードで、の追加のオーバーヘッドを発生させることなく、効率的な再帰的ラムダを簡単に作成できます(ユーザーが誤ってコピーを作成するのを防ぐため、元のコードを少し編集するだけで) ):

template <class F>
struct y_combinator {
    F f; // the lambda will be stored here

    // a forwarding operator():
    template <class... Args>
    decltype(auto) operator()(Args&&... args) const {
        // we pass ourselves to f, then the arguments.
        // [edit: Barry] pass in std::ref(*this) instead of *this
        return f(std::ref(*this), std::forward<Args>(args)...);
    }
};

// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
    return {std::forward<F>(f)};
}

元のsum試みは次のようになります。

auto sum = make_y_combinator([term,next](auto sum, int a, int b) {
  if (a>b) {
    return 0;
  }
  else {
    return term(a) + sum(next(a),b);
  }
});

C ++ 17では、CTADを使用して、控除ガイドを追加できます。

template <class F> y_combinator(F) -> y_combinator<F>;

これにより、ヘルパー関数の必要がなくなります。y_combinator{[](auto self, ...){...}}直接書けます。


C ++ 20では、集計にCTADを使用すると、控除ガイドは必要ありません。


これは素晴らしいですが、最後の行のstd::forward<decltype(sum)>(sum)代わりに検討することができsumます。
Johan Lundberg、2017

@ヨハンいいえ、1つしかoperator()ないため、転送しても何も得られませんsum
バリー

ああ、そうだね。転送なしの転送参照の使用には使用されません。
Johan Lundberg、2017

Yコンビネーターは確かに行く方法です。ただしconst、提供された関数オブジェクトに非const呼び出し演算子がある場合は、非オーバーロードを追加する必要があります。そして、SFINAEを使用してnoexcept、両方に対して計算します。また、C ++ 17ではmaker-functionは不要になりました。
Deduplicator '25

2
@minexはい、auto sumコピーしますがreference_wrapper、参照を取得するのと同じです。実装で一度行うと、誤ってコピーしてしまうことはありません。
バリー

22

私は別の解決策を持っていますが、ステートレスラムダでのみ機能します。

void f()
{
    static int (*self)(int) = [](int i)->int { return i>0 ? self(i-1)*i : 1; };
    std::cout<<self(10);
}

ここでのトリックは、ラムダが静的変数にアクセスでき、ステートレス変数を関数ポインターに変換できることです。

標準のラムダで使用できます。

void g()
{
    int sum;
    auto rec = [&sum](int i) -> int
    {
        static int (*inner)(int&, int) = [](int& _sum, int i)->int 
        {
            _sum += i;
            return i>0 ? inner(_sum, i-1)*i : 1; 
        };
        return inner(sum, i);
    };
}

GCC 4.7での作業


3
これはstd :: functionよりも優れたパフォーマンスを持つはずなので、代わりに+1します。しかし、実際には、この時点でラムダを使用するのが最善の選択肢かどうか疑問に思います;)
Antoine

ステートレスラムダがある場合は、完全な機能にすることもできます。
Timmmm 2018

1
@Timmmmしかし、その後、実装の一部を外部の言葉に漏らします。通常、ラムダは親関数と密接に結合しています(キャプチャがない場合でも)。そうでない場合は、最初にラムダを使用せず、ファンクタの通常の関数を使用する必要があります。
ヤンクス2018

10

ラムダ関数自体を再帰的に呼び出すことができます。あなたがしなければならない唯一のことは、コンパイラがそれが戻り値と引数の型であることを知っているように、関数ラッパーを通してそれを参照することです(まだ定義されていない変数-ラムダ自体-をキャプチャすることはできません) 。

  function<int (int)> f;

  f = [&f](int x) {
    if (x == 0) return 0;
    return x + f(x-1);
  };

  printf("%d\n", f(10));

ラッパーfのスコープを使い果たしないように十分注意してください。


3
ただし、これは受け入れられた回答と同じであり、std関数を使用するとペナルティが発生する可能性があります。
Johan Lundberg

9

外部クラスと関数(std::functionまたは固定小数点コンビネーターなど)を使用せずにラムダを再帰的にするには、C ++ 14で次の構成を使用できます(実例):

#include <utility>
#include <list>
#include <memory>
#include <iostream>

int main()
{
    struct tree
    {
        int payload;
        std::list< tree > children = {}; // std::list of incomplete type is allowed
    };
    std::size_t indent = 0;
    // indication of result type here is essential
    const auto print = [&] (const auto & self, const tree & node) -> void
    {
        std::cout << std::string(indent, ' ') << node.payload << '\n';
        ++indent;
        for (const tree & t : node.children) {
            self(self, t);
        }
        --indent;
    };
    print(print, {1, {{2, {{8}}}, {3, {{5, {{7}}}, {6}}}, {4}}});
}

プリント:

1
 2
  8
 3
  5
   7
  6
 4

ラムダの結果タイプは明示的に指定する必要があることに注意してください。


6

std::function<>キャプチャーメソッドを使用して、再帰関数と再帰ラムダ関数を比較するベンチマークを実行しました。clangバージョン4.1で完全な最適化を有効にすると、ラムダバージョンの実行速度が大幅に低下しました。

#include <iostream>
#include <functional>
#include <chrono>

uint64_t sum1(int n) {
  return (n <= 1) ? 1 : n + sum1(n - 1);
}

std::function<uint64_t(int)> sum2 = [&] (int n) {
  return (n <= 1) ? 1 : n + sum2(n - 1);
};

auto const ITERATIONS = 10000;
auto const DEPTH = 100000;

template <class Func, class Input>
void benchmark(Func&& func, Input&& input) {
  auto t1 = std::chrono::high_resolution_clock::now();
  for (auto i = 0; i != ITERATIONS; ++i) {
    func(input);
  }
  auto t2 = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
  std::cout << "Duration: " << duration << std::endl;
}

int main() {
  benchmark(sum1, DEPTH);
  benchmark(sum2, DEPTH);
}

結果を生成します:

Duration: 0 // regular function
Duration: 4027 // lambda function

(注:コンパイル時の評価を排除するために、cinから入力を取得するバージョンでも確認しました)

Clangはコンパイラ警告も生成します。

main.cc:10:29: warning: variable 'sum2' is uninitialized when used within its own initialization [-Wuninitialized]

これは予想されることであり、安全ですが、注意する必要があります。

私たちのツールベルトに解決策があることは素晴らしいことですが、パフォーマンスが現在の方法に匹敵する場合、言語はこのケースを処理するためのより良い方法を必要とすると思います。

注意:

コメンターが指摘したように、VC ++の最新バージョンはこれを同等のパフォーマンスの点まで最適化する方法を見つけたようです。おそらく、これを処理するためのより良い方法は必要ないかもしれません(構文糖を除く)。

また、他のいくつかのSO投稿がここ数週間で概説したstd::function<>ように、少なくともラムダキャプチャが大きすぎstd::functionて小さなファンクタのライブラリ最適化スペースの使用に収まらない場合は、それ自体のパフォーマンスが直接関数を呼び出すのと比べてスローダウンの原因である可能性があります(私はちょっとしたさまざまな短い文字列の最適化が好きだと思いますか?)


2
-1。「lambda」バージョンが長くなる唯一の理由は、それをstd :: functionにバインドすることです。これにより、operator()が仮想呼び出しを呼び出し、明らかに時間がかかります。その上、VS2012リリースモードのコードでは、どちらの場合もほぼ同じ時間がかかりました。
Yam Marcovic 2013

@YamMarcovicなに?これが現在、再帰的ラムダを記述する唯一の既知の方法です(これが例の要点でした)。VS2012がこのユースケースを最適化する方法を見つけたことを非常にうれしく思います(最近、このトピックに関する開発が進んでいますが、ラムダがより多くキャプチャした場合、std :: function small-メモリファンクタの最適化など)。
mmocny 2013

2
認めた。あなたの投稿を誤解しました。その後+1。ああ、あなたがこの回答を編集した場合にのみ賛成票を投じることができます。コメントなど、もう少し強調していただけますか?
Yam Marcovic 2013

1
@YamMarcovic完了しました。フィードバックを提供し、必要に応じて改善していただけるとありがたいです。あなたに+1、良い先生。
mmocny 2013

0時間は通常、「操作全体が最適化されて離れた」ことを意味します。コンパイラが計算の再処理で何もしないことをコンパイラが証明した場合、cinから入力を取得しても何も起こりません。
Yakk-Adam Nevraumont 2017年

1

これは、フィックスポイント演算子のやや単純な実装であり、何が起こっているのかを少し明確にします。

#include <iostream>
#include <functional>

using namespace std;

template<typename T, typename... Args>
struct fixpoint
{
    typedef function<T(Args...)> effective_type;
    typedef function<T(const effective_type&, Args...)> function_type;

    function_type f_nonr;

    T operator()(Args... args) const
    {
        return f_nonr(*this, args...);
    }

    fixpoint(const function_type& p_f)
        : f_nonr(p_f)
    {
    }
};


int main()
{
    auto fib_nonr = [](const function<int(int)>& f, int n) -> int
    {
        return n < 2 ? n : f(n-1) + f(n-2);
    };

    auto fib = fixpoint<int,int>(fib_nonr);

    for (int i = 0; i < 6; ++i)
    {
        cout << fib(i) << '\n';
    }
}

std::function関数ポインタ(コアの場合は通常の関数とステートレスラムダでのみ機能します)に置き換えると、答え(パフォーマンスに関して)を改善できると思います。ところでfib_nonr受け入れるべきfixpoint<int,int>あなたが使用している場合、std::functionそのからの新しいコピーをクレート必要とします*this
ヤンクス2014年

1

これは、@ Barryによって提案されたものに基づいた、Yコンビネーターソリューションの改良バージョンです。

template <class F>
struct recursive {
  F f;
  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)  const { return f(std::ref(*this), std::forward<Ts>(ts)...); }

  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)  { return f(std::ref(*this), std::forward<Ts>(ts)...); }
};

template <class F> recursive(F) -> recursive<F>;
auto const rec = [](auto f){ return recursive{std::move(f)}; };

これを使用するには、次のようにします

auto fib = rec([&](auto&& fib, int i) {
// implementation detail omitted.
});

let recOCaml のキーワードと似ていますが、同じではありません。


0

C ++ 14:1、20からすべての数値を出力する、ラムダの再帰的な匿名のステートレス/キャプチャジェネリックなしセットを次に示します

([](auto f, auto n, auto m) {
    f(f, n, m);
})(
    [](auto f, auto n, auto m) -> void
{
    cout << typeid(n).name() << el;
    cout << n << el;
    if (n<m)
        f(f, ++n, m);
},
    1, 20);

私が正しく理解していれば、これはYコンビネーターソリューションを使用しています

そして、これがsum(n、m)バージョンです

auto sum = [](auto n, auto m) {
    return ([](auto f, auto n, auto m) {
        int res = f(f, n, m);
        return res;
    })(
        [](auto f, auto n, auto m) -> int
        {
            if (n > m)
                return 0;
            else {
                int sum = n + f(f, n + 1, m);
                return sum;
            }
        },
        n, m); };

auto result = sum(1, 10); //result == 55

-1

これがOPの最終的な回答です。とにかく、Visual Studio 2010はグローバル変数のキャプチャをサポートしていません。また、定義によってグローバル変数にグローバルにアクセスできるため、それらをキャプチャする必要はありません。次の回答では、代わりにローカル変数を使用しています。

#include <functional>
#include <iostream>

template<typename T>
struct t2t
{
    typedef T t;
};

template<typename R, typename V1, typename V2>
struct fixpoint
{
    typedef std::function<R (V1, V2)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t {
    public:
        func_t operator()(loopfunc_t v)const {
            return func(v);
        }
        template<typename L>
        loopfunc_t(const L &l):func(l){}
        typedef V1 Parameter1_t;
        typedef V2 Parameter2_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    };
    static yfunc_t fix;
};
template<typename R, typename V1, typename V2>
typename fixpoint<R, V1, V2>::yfunc_t fixpoint<R, V1, V2>::fix = [](tfunc_t f) -> func_t {
    return [f](fixpoint<R, V1, V2>::loopfunc_t x){  return f(x(x)); }
    ([f](fixpoint<R, V1, V2>::loopfunc_t x) -> fixpoint<R, V1, V2>::func_t{
        auto &ff = f;
        return [ff, x](t2t<decltype(x)>::t::Parameter1_t v1, 
            t2t<decltype(x)>::t::Parameter1_t v2){
            return ff(x(x))(v1, v2);
        }; 
    });
};

int _tmain(int argc, _TCHAR* argv[])
{
    auto term = [](int a)->int {
      return a*a;
    };

    auto next = [](int a)->int {
      return ++a;
    };

    auto sum = fixpoint<int, int, int>::fix(
    [term,next](std::function<int (int, int)> sum1) -> std::function<int (int, int)>{
        auto &term1 = term;
        auto &next1 = next;
        return [term1, next1, sum1](int a, int b)mutable ->int {
            if(a>b)
                return 0;
        else
            return term1(a) + sum1(next1(a),b);
        };
    });

    std::cout<<sum(1,10)<<std::endl; //385

    return 0;
}

この回答をコンパイラに依存しないようにすることは可能ですか?
rayryeng

-2

定義中の変数(合計)をキャプチャしようとしています。それは良くありません。

私は本当に自己再帰的なC ++ 0xラムダが可能だとは思いません。ただし、他のラムダをキャプチャできるはずです。


3
ただし、キャプチャリストを変更せずに合計の宣言を「auto」からstd :: function <int(int、int)>に変更すると機能します。
weima

それはもはやラムダではないので、ラムダの代わりに使用できる関数ですか?
Hamish Grubijan、2010年

-2

この答えはヤンケスのものより劣りますが、それでも、ここにそれは行きます:

using dp_type = void (*)();

using fp_type = void (*)(dp_type, unsigned, unsigned);

fp_type fp = [](dp_type dp, unsigned const a, unsigned const b) {
  ::std::cout << a << ::std::endl;
  return reinterpret_cast<fp_type>(dp)(dp, b, a + b);
};

fp(reinterpret_cast<dp_type>(fp), 0, 1);

避けるべきだと思いますreinterpret_cast。あなたのケースでおそらく最良の方法は、を置き換える構造体を作成することdp_typeです。これにはフィールドが必要でfp_type、から構築でき、のような引数fp_typeを持つ演算子()を持つことができますfp_type。これは近いstd::functionですが、自己参照引数を許可します。
ヤンクス2014

構造体なしで最小限の例を投稿したかったので、私の回答を自由に編集して、より完全なソリューションを提供してください。A structは、さらに間接的なレベルを追加します。この例は機能し、キャストは標準に準拠してい-1ます。何のために使用したのかわかりません。
user1095108 2014

いいえ、構造体はポインタのコンテナとしてのみ機能し、値として渡されます。これは、ポインタよりも間接的またはオーバーヘッドではありません。約そして-1、私はあなたにそれを与える人知らなかったが、私が考えるそのためには、reinterpret_cast最後の手段として使用する必要があります。
ヤンクス2014

これcastはおそらくc ++ 11標準で動作することが保証されています。使用してstruct、私の目には、ラムダオブジェクトの使用を倒すことができました。結局のところ、struct提案するのはラムダオブジェクトを利用するファンクタです。
user1095108 14

@Pseudonymソリューションを見て、削除するだけでstd::function、私が考えていたものに近いものになります。これはおそらくソリューションと同様のパフォーマンスになります。
ヤンクス2014年

-3

固定小数点コンビネーターが必要です。参照してくださいこれを

または、次のコードを見てください。

//As decltype(variable)::member_name is invalid currently, 
//the following template is a workaround.
//Usage: t2t<decltype(variable)>::t::member_name
template<typename T>
struct t2t
{
    typedef T t;
};

template<typename R, typename V>
struct fixpoint
{
    typedef std::function<R (V)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t {
    public:
        func_t operator()(loopfunc_t v)const {
            return func(v);
        }
        template<typename L>
        loopfunc_t(const L &l):func(l){}
        typedef V Parameter_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    };
    static yfunc_t fix;
};
template<typename R, typename V>
typename fixpoint<R, V>::yfunc_t fixpoint<R, V>::fix = 
[](fixpoint<R, V>::tfunc_t f) -> fixpoint<R, V>::func_t {
    fixpoint<R, V>::loopfunc_t l = [f](fixpoint<R, V>::loopfunc_t x) ->
        fixpoint<R, V>::func_t{
            //f cannot be captured since it is not a local variable
            //of this scope. We need a new reference to it.
            auto &ff = f;
            //We need struct t2t because template parameter
            //V is not accessable in this level.
            return [ff, x](t2t<decltype(x)>::t::Parameter_t v){
                return ff(x(x))(v); 
            };
        }; 
        return l(l);
    };

int _tmain(int argc, _TCHAR* argv[])
{
    int v = 0;
    std::function<int (int)> fac = 
    fixpoint<int, int>::fix([](std::function<int (int)> f)
        -> std::function<int (int)>{
        return [f](int i) -> int{
            if(i==0) return 1;
            else return i * f(i-1);
        };
    });

    int i = fac(10);
    std::cout << i; //3628800
    return 0;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.