web-dev-qa-db-ja.com

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

私は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);
};
_

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

117
weima

autoバージョンと完全に指定されたタイプバージョンの違いを考えてください。 autoキーワードは、初期化されたものからそのタイプを推測しますが、初期化するものはそのタイプを知る必要があります(この場合、ラムダクロージャーはキャプチャしているタイプを知る必要があります) 。鶏と卵の問題のようなもの。

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

コードのこのわずかな変更を検討すると、より意味があります。

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では機能します)。それは、型推論と実際には互換性がないということです。

153
I. M. McIntosh

トリックは、ラムダ実装をそれ自体に取り込むことですパラメータとしてで、キャプチャではありません。

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/

does C++ 14が必要ですが、質問はC++ 11にありますが、おそらくほとんどの人にとって興味深いものです。

std::functionを経由することも可能ですが、canの場合、コードが遅くなります。しかしいつもではない。 std :: function vs template への回答をご覧ください

49
Johan Lundberg

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

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

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での動作

21
Yankes

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

  function<int (int)> f;

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

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

ラッパーfのスコープを超えないように注意してください。

10
Zuza

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

#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

ラムダの結果タイプは明示的に指定する必要があります。

8
Orient

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が小さなファンクターに使用します(さまざまな短い文字列の最適化と似ていると思いますか?)。

6
mmocny

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

#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';
    }
}
1
Pseudonym

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
0
Jonas Brandel