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

155

Я новичок в 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: error: ' ((<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);
};

Может кто-нибудь пролить свет на это?

Weima
источник
Могут ли это быть статические или неявно динамические объявления?
Hamish Grubijan
3
Что там mutableделает ключевое слово?
Приветствия и hth. - Alf
Захват переменных с неавтоматической продолжительностью хранения не допускается. Вы должны сделать это так: chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow
Просто к сведению, во втором фрагменте кода ваша лямбда слишком многословна, std::function<int(int,int)> sum = [&](int a, int b) {
примите во

Ответы:

199

Подумайте о разнице между автоматической версией и полностью указанной типовой версией. В авто выводит ключевое слово его тип от того, что он инициализированный, но что вы инициализирующего его потребности , чтобы знать , что его типа (в данном случае потребность закрытия лямбды знать типы это захватив). Что-то вроде проблемы с курицей и яйцом.

С другой стороны, полностью заданному типу функционального объекта не нужно «знать» что-либо о том, что ему присваивается, и поэтому лямбда-замыкание может точно так же быть полностью информировано о типах, которые он захватывает.

Рассмотрите эту небольшую модификацию вашего кода, и это может иметь больше смысла:

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
Я не согласен с этим. Тип лямбда хорошо известен сразу после ввода тела функции - нет причин, по которым его не следует выводить к тому времени.
Puppy
17
@DeadMG, но спецификация запрещает ссылаться на autoпеременную в ее инициализаторе. тип автоматической переменной еще не известен, когда инициализатор обрабатывается.
Йоханнес Шауб - лит
2
Хотите знать, почему это не помечено как «ответ», а тот Python классифицируется как «Ответ» ?!
Ajay
1
@Puppy: Однако в случае неявного захвата для эффективности фактически захватываются только ссылочные переменные, поэтому тело необходимо проанализировать.
kec
1
Есть ли допустимая интерпретация для sumдругого std::function<int(int, int)>, или спецификация C ++ просто не позаботилась об этом?
Mateen Ulhaq
97

Хитрость заключается в том, чтобы передать лямбда-реализацию себе в качестве параметра , а не путем захвата.

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 template


Это не просто особенность 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
21
это намного лучше, чем std :: function по трем причинам: он не требует стирания типа или выделения памяти, он может быть constexpr и правильно работает с автоматическими (шаблонными) параметрами / типом возвращаемого значения
Иван Санс-Караса,
3
Предположительно, это решение также имеет то преимущество, что его можно копировать без выхода ссылки std :: function за пределы области видимости?
Ури Гранта
7
Хм, при попытке GCC 8.1 (linux) жаловался: error: use of ‘[...]’ before deduction of ‘auto’- необходимо явно указать тип возвращаемого значения (с другой стороны, не нужно изменять).
Аконкагуа
@Aconcagua то же самое здесь с Xcode10, и я установил стандарт C ++ на 17 даже
IceFire
42

С 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
Барри
О, это правда. Не привык использовать пересылочные ссылки без пересылки.
Йохан Лундберг
1
Y-комбинатор, безусловно, лучший вариант. Но вам действительно следует добавить неперегрузку constв случае, если предоставленный объект-функция не имеет constоператора вызова. И используйте SFINAE и вычислите noexceptдля обоих. Кроме того, в C ++ 17 больше нет необходимости в функции производителя.
Дедупликатор
3
@minex Да, auto sumкопирует ... но копирует reference_wrapper, что то же самое, что брать ссылку. Выполнение этого один раз в реализации означает, что ни одно из применений никогда не будет случайно скопировано.
Барри
24

У меня есть другое решение, но работаю только с лямбдами без состояния:

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 в качестве альтернативы. Но на самом деле, на данный момент мне интересно, является ли использование лямбд лучшим вариантом;)
Антуан
1
Если у вас есть лямбда без сохранения состояния, вы можете просто сделать ее полноценной функцией.
Timmmm
1
@Timmmm Но тогда вы пропускаете часть реализации во внешнее слово, обычно лямбды тесно связаны с родительской функцией (даже без захвата). Если это не так, вам не следует использовать лямбды в первую очередь, а использовать обычные функции функторов.
Yankes
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.
Йохан Лундберг
10

Чтобы сделать лямбда-рекурсивным без использования внешних классов и функций (например, 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для библиотеки пространства, используемые для малых функторов. (Я думаю, вроде как различные оптимизации коротких строк?).

mmocny
источник
3
-1. Обратите внимание, что единственная причина, по которой версия "лямбда" занимает больше времени, заключается в том, что вы привязываете ее к std :: function, что заставляет operator () вызывать виртуальный вызов, а это, очевидно, займет больше времени. Вдобавок ко всему, ваш код в режиме выпуска VS2012 занял примерно одинаковое количество времени в обоих случаях.
Ям Маркович
@YamMarcovic Что? В настоящее время это единственный известный способ написания рекурсивной лямбды (это было целью примера). Мне очень приятно узнать, что VS2012 нашел способ оптимизировать этот вариант использования (хотя в последнее время по этой теме было больше разработок, очевидно, если бы моя лямбда захватила больше, она не поместилась бы в std :: function small- оптимизация функтора памяти или еще много чего).
mmocny
2
Подтверждено. Я неправильно понял твой пост. +1 тогда. Ага, проголосовать можно, только если вы отредактируете этот ответ. Не могли бы вы выделить это немного больше, например, в комментарии?
Ям Маркович
1
@YamMarcovic Готово. Я ценю вашу готовность предоставить отзыв и при необходимости доработать его. +1 вам, добрый сэр.
mmocny
Время 0 обычно означает, что «вся операция была оптимизирована». Ввод данных из cin ничего не дает, если компилятор доказывает, что вы ничего не делаете с результатом ваших вычислений.
Якк - Адам Неврамонт
2

Вот усовершенствованная версия решения 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

Это немного более простая реализация оператора фиксированной точки, которая делает более очевидным, что именно происходит.

#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.
Yankes 01
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

Вот окончательный ответ на 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
Потому что это уже не лямбда, а функция, которую можно использовать вместо лямбды?
Хэмиш Грубиджан
-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);
user1095108
источник
Я думаю, тебе следует избегать reinterpret_cast. Вероятно, лучший способ в вашем случае - создать структуру, которая заменяет dp_type. Он должен иметь поле fp_type, может быть построен из fp_typeоператора ()с такими аргументами, как fp_type. Это будет близко к std::functionаргументу с самооценкой, но позволит.
Янкес
Я хотел опубликовать минимальный пример без структуры, не стесняйтесь редактировать свой ответ и предоставить более полное решение. А structтакже добавит дополнительный уровень косвенности. Пример работает, и приведение соответствует стандарту, я не знаю, для чего он -1был.
user1095108
нет, структура будет работать только как контейнер для указателя и будет передана как значение. Это будет не больше косвенного обращения или дополнительных затрат, чем указатель. И насчет того, что -1я не знал, кто дал это вам, но я думаю, что это потому, что reinterpret_castследует использовать в крайнем случае.
Янкес,
castЯкобы гарантированно работать на C ++ 11 стандарта. На structмой взгляд, использование a может помешать использованию лямбда-объекта. В конце концов, structвы предлагаете функтор, использующий лямбда-объект.
user1095108
Посмотрите на решение @Pseudonym, удалите только, std::functionи у вас будет что-то близкое к тому, что я имел в виду. Вероятно, это будет иметь производительность, аналогичную вашему решению.
Yankes 01
-4

Вам нужен комбинатор с фиксированной точкой. Смотрите это .

или посмотрите следующий код:

//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;
}
Земляной двигатель
источник