Рекурсивні лямбда-функції в C ++ 11


143

Я новачок у С ++ 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);
};

Може хтось, будь ласка, кине світло на це?


Чи може це статична та неявна динамічна декларація?
Гаміш Грубіян

3
Що тут mutableробить ключове слово?
Ура та хт. - Альф

Захоплення змінних з неавтоматичною тривалістю зберігання не дозволяється. Ви повинні зробити це так: chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow

Просто FYI, у другому фрагменті коду ваша лямбда занадто багатослівна, врахуйте цю зміну: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);
};

Очевидно, що це не буде працювати з авто . Рекурсивні лямбда-функції працюють чудово (принаймні, вони працюють у MSVC, де я маю досвід роботи з ними), це просто те, що вони насправді не сумісні з типом виводу.


3
Я не згоден з цим. Тип лямбда добре відомий, як тільки введено функціональне тіло - немає причин, щоб його не слід було виводити до того часу.
Щеня

16
@DeadMG, але spec забороняє посилатися на autoзмінну в ініціалізаторі. тип автоматичної змінної ще не відомий, коли ініціалізатор обробляється.
Йоханнес Шауб - ліб

1
Цікаво, чому це не позначено як "відповідь", а той Python класифікується як "Відповідь" ?!
Аджай

1
@Puppy: У випадку неявного захоплення, однак, для ефективності фактично фіксуються лише посилання, що посилаються на зміну, тому тіло потрібно проаналізувати.
kec

Чи існує правильна інтерпретація, sumкрім якої std::function<int(int, int)>, чи специфікація C ++ просто не намагається зробити це?
Матін Ульхак

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 ++ 14 , а питання на C ++ 11, але , можливо , цікаво більшості.

Перехід 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

17
це набагато краще, ніж std :: функція з 3 причин: вона не вимагає стирання типу або розподілу пам’яті, вона може бути constexpr і працює належним чином з автоматичними (шаблоновими) параметрами / типом повернення
Іван Санц-Караса

3
Імовірно, це рішення також має перевагу в тому, що воно може бути скопійоване без посилання std :: на функцію?
Урі Гранта

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 Ні, є лише один, operator()тому нічого не можна отримати, переадресувавшиsum
Баррі

О, це правда. Не використовується для використання посилань для переадресації без переадресації.
Йохан Лундберг

Y-комбінатор, безумовно, це шлях. Але вам дійсно слід додати constнеперевантаження, якщо наданий об’єкт функції має constоператора без виклику. І використовувати SFINAE та обчислити noexceptдля обох. Крім того, більше немає необхідності в функції виробника в C ++ 17.
Дедуплікатор

2
@minex Так, auto sumкопії ... але він копіює a 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 для альтернативи. Але дійсно, на даний момент мені цікаво, чи найкраще використовувати використання лямбда;)
Антуан

Якщо у вас лямбда без громадянства, ви можете просто зробити її повноцінною функцією.
Timmmm

1
@Timmmm Але тоді ви просочуєте частину реалізації до зовнішнього слова, як правило, лямбди тісно поєднані з батьківською функцією (навіть коли не виходить). Якщо цього не було, то не слід в першу чергу використовувати лямбда і використовувати звичайні функції функторів.
Янкс

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.
Йохан Лундберг

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<>метод захоплення. Коли ввімкнена повна оптимізація в кланг версії 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. Зауважте, що єдина причина, що версія "лямбда" займає більше часу, це те, що ви прив'язуєте її до функції std ::, завдяки якій оператор () викликає віртуальний виклик, і це, очевидно, займе більше часу. Крім того, ваш код у режимі випуску VS2012 зайняв приблизно однакову кількість часу в обох випадках.
Ям Маркович

@YamMarcovic Що? Наразі це єдиний відомий спосіб написання рекурсивної лямбда (це було суть прикладу). Мені дуже приємно знати, що VS2012 знайшов спосіб оптимізувати цей випадок використання (хоча останнім часом на цю тему було більше розробок, мабуть, якби мій лямбда захопив більше, він би не помістився в рамках std :: function small- оптимізації функціонера пам'яті чи що-небудь ще).
mmocny

2
Визнаний. Я неправильно зрозумів вашу посаду. +1 потім. Гах, може подати заявку лише в тому випадку, якщо ви редагуєте цю відповідь. То ви могли б трохи підкреслити це, як, наприклад, у коментарі?
Ям Маркович

1
@YamMarcovic Готово. Я вдячний за вашу готовність надати відгуки та уточнити їх у разі потреби. +1 вам, добрий пане.
mmocny

0 разів зазвичай означає "вся операція була оптимізована". Введення даних із cin не робить нічого, якщо компілятор докаже, що ви нічого не робите з переповненими обчисленнями.
Якк - Адам Невраумон

1

Це дещо простіша реалізація оператора fixpoint, яка робить трохи більш очевидним, що саме відбувається.

#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на покажчик функції (з ядер це буде працювати лише з нормальною функцією та лямбдами без стану). Btw fib_nonrповинен прийняти fixpoint<int,int>, якщо ви використовуєте std::functionйого вимагати створення нової копії з *this.
Янкі

1

Ось доопрацьована версія рішення Y-комбінатора на основі запропонованої @Barry.

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 recключове слово в OCaml, хоча і не те саме.


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-комбінатора

І ось сума (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

Ось остаточна відповідь на ОП. У будь-якому випадку, 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

Тому що це вже не лямбда, а функція, яку можна використовувати замість лямбда?
Гаміш Грубіян

-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але дозволить самостійно посилатися на аргумент.
Yankes

Я хотів опублікувати мінімальний приклад, без структури, не соромтесь відредагувати свою відповідь і надати більш повне рішення. А structтакож додав би додатковий рівень непрямості. Приклад працює і акторський склад відповідає стандарту, я не знаю, для чого це -1було.
користувач1095108

ні, struct працюватиме лише як контейнер для покажчика і передаватиметься як значення. Це буде не більше непрямої чи накладної, ніж покажчик. І про те, що -1я не знаю, хто вам це дає, але я вважаю, що це reinterpret_castслід використовувати в крайньому випадку.
Янки

castНібито гарантовано працювати на C ++ 11 стандарту. Використання structв моїх очах може перемогти використання лямбда-об’єкта. Зрештою, structти , що ти пропонуєш, - це функтор, що використовує лямбда-об’єкт.
користувач1095108

Подивіться на рішення @Pseudonym, видаліть тільки, std::functionі у вас буде щось близьке до того, що я мав на увазі. Це, ймовірно, матиме аналогічні показники вашого рішення.
Янкі

-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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.