Рекурсивные лямбда-колбэки без Y Combinator

Я хочу создать обратный вызов, который рекурсивно возвращает себя в качестве обратного вызова.

Предлагаемый метод для рекурсии состоит в том, чтобы функция имела ссылку на себя:

std::function<void (int)> recursive_function = [&] (int recurse) {
    std::cout << recurse << std::endl;

    if (recurse > 0) {
        recursive_function(recurse - 1);
    }
};

Это терпит неудачу, как только вы возвращаете его из функции:

#include <functional>
#include <iostream>

volatile bool no_optimize = true;

std::function<void (int)> get_recursive_function() {
    std::function<void (int)> recursive_function = [&] (int recurse) {
        std::cout << recurse << std::endl;

        if (recurse > 0) {
            recursive_function(recurse - 1);
        }
    };

    if (no_optimize) {
        return recursive_function;
    }

    return [] (int) {};
}

int main(int, char **) {
    get_recursive_function()(10);
}

который дает ошибку сегментации после вывода 10 потому что ссылка становится недействительной.

Как мне это сделать? Я успешно использовал то, что я считаю Y Combinator (который я опубликую в качестве ответа), но это сильно сбивает с толку. Есть ли способ лучше?


Другие попытки

Я попробовал скучный подход обернуть его в другой слой обратных вызовов:

#include <functional>
#include <iostream>
#include <memory>

volatile bool no_optimize = true;

std::function<void (int)> get_recursive_function() {
    // Closure to allow self-reference
    auto recursive_function = [] (int recurse) {
        // Actual function that does the work.
        std::function<void (int)> function = [&] (int recurse) {
            std::cout << recurse << std::endl;

            if (recurse > 0) {
                function(recurse - 1);
            }
        };

        function(recurse);
    };

    if (no_optimize) {
        return recursive_function;
    }

    return [] (int) {};
}

int main(int, char **) {
    get_recursive_function()(10);
}

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

#include <functional>
#include <iostream>
#include <memory>
#include <queue>

volatile bool no_optimize = true;

std::queue<std::function<void (void)>> callbacks;

std::function<void (int)> get_recursive_function() {
    // Closure to allow self-reference
    auto recursive_function = [] (int recurse) {
        // Actual function that does the work.
        std::function<void (int)> function = [&] (int recurse) {
            std::cout << recurse << std::endl;

            if (recurse > 0) {
                callbacks.push(std::bind(function, recurse - 1));
            }
        };

        function(recurse);
    };

    if (no_optimize) {
        return recursive_function;
    }

    return [] (int) {};
}

int main(int, char **) {
    callbacks.push(std::bind(get_recursive_function(), 10));

    while (!callbacks.empty()) {
        callbacks.front()();
        callbacks.pop();
    }
}

который дает 10, затем 9 а затем ошибки сегментации.

4 ответа

Как вы правильно заметили, существует неверная ссылка из лямбда-захвата [&],

Ваши возвращаемые значения являются функторами различного рода, поэтому я предполагаю, что точный тип возвращаемого значения не важен, просто то, что он ведет себя как функция, то есть может быть вызвано.

Если recursive_function завернут в struct или же class Вы можете сопоставить оператора вызова с recursive_function член. Проблема возникает с захватом this переменная. Это будет захвачено с this при создании, поэтому, если объект копируется немного, оригинал this может больше не быть действительным. Так что соответствующий this может быть передано в функцию во время выполнения (это this проблема может не быть проблемой, но она сильно зависит от того, когда и как вы вызываете функцию).

#include <functional>
#include <iostream>

volatile bool no_optimize = true;

struct recursive {
    std::function<void (recursive*, int)> recursive_function = [] (recursive* me, int recurse) {
        std::cout << recurse << std::endl;

        if (recurse > 0) {
            me->recursive_function(me, recurse - 1);
        }
    };

    void operator()(int n)
    {
        if (no_optimize) {
            recursive_function(this, n);
        }
    }
};

recursive get_recursive_function() {
    return recursive();
}

int main(int, char **) {
    get_recursive_function()(10);
}

В качестве альтернативы, если recursive_function может быть static тогда объявление его в оригинальном примере кода также может помочь вам.

Я хотел добавить некоторую общность к ответу выше, то есть сделать его шаблоном;

#include <functional>
#include <iostream>

volatile bool no_optimize = true;

template <typename Signature>
struct recursive;

template <typename R, typename... Args>
struct recursive<R (Args...)> {
    std::function<R (recursive const&, Args... args)> recursive_function;

    recursive() = default;

    recursive(decltype(recursive_function) const& func) : recursive_function(func)
    {
    }

    template <typename... T>
    R operator()(T&&... args) const
    {
        return recursive_function(*this, std::forward<Args>(args)...);
    }
};

recursive<void (int)> get_recursive_function()
{
    using result_type = recursive<void (int)>;

    if (!no_optimize) {
        return result_type();
    }

    result_type result ([](result_type const& me, int a) {
        std::cout << a << std::endl;

        if (a > 0) {
            me(a - 1);
        }
    });

    return result;
}

int main(int, char **) {
    get_recursive_function()(10);
}

Как это работает? По сути, он перемещает рекурсию изнутри функции (т.е. вызывает себя) к объекту (то есть оператору функции на самом объекте) для реализации рекурсии. в get_recursive_function тип результата recursive<void (int)> используется в качестве первого аргумента для рекурсивной функции. это const& потому что я реализовал operator() как const в соответствии с большинством стандартных алгоритмов и по умолчанию для лямбда-функции. Это требует некоторого "сотрудничества" с исполнителем функции (то есть использование me параметр; само по себе *this), чтобы рекурсия работала, но за эту цену вы получаете рекурсивную лямбду, которая не зависит от ссылки на стек.

Все проблемы в программировании могут быть решены с помощью другого уровня косвенности, за исключением слишком большого числа уровней косвенности.

Моя цель - создать тип recursive<void(int)> это позволяет вам легко создавать рекурсивную лямбду. Для этого вы переходите в лямбду с подписью void(recursive<void(int)>, int) - первый аргумент - это то, что вы вызываете, чтобы выполнить рекурсивный вызов.

Затем я завязываю его в узлы и делаю его полностью рекурсивной функцией с подписью void(int),

Вот моя реализация recursive<Signature>:

template<class Sig>
struct recursive;
template<class R, class... As>
struct recursive< R(As...) > {
  using base_type = std::function<R(recursive, As...)>;
private:
  std::shared_ptr< base_type > base;
public:

  template<typename...Ts>
  auto operator()(Ts&&... ts) const
  -> typename std::result_of< base_type( recursive, Ts... ) >::type
  {
    return (*base)(*this, std::forward<Ts>(ts)...);
  }

  recursive(recursive const&)=default;
  recursive(recursive&&)=default;
  recursive& operator=(recursive const&)=default;
  recursive& operator=(recursive &&)=default;
  recursive() = default;
  template<typename L, typename=typename std::result_of< L(recursive, As...) >::type>
  explicit recursive( L&& f ):
    base( std::make_shared<base_type>(std::forward<L>(f)))
  {}
  explicit operator bool() const { return base && *base; }
};

Это, правда, довольно сложно. Я сделал несколько вещей, чтобы сделать его более эффективным, такой идеальный пересылка. Это также, в отличие от std::function, дважды проверяет, что тип лямбда, который вы передаете ему, соответствует желаемой подписи.

Я верю, но не подтвердил, что я сделал дружелюбным сделать подпись лямбды void(auto&&,int), Кто-нибудь знает полностью совместимый онлайн-компилятор C++1y?

Выше это просто шаблон. Важно то, как это выглядит в точке использования:

std::function<void (int)> get_recursive_function() {
  auto f =
    [] (recursive<void(int)> self, int recurse) {
      std::cout << recurse << std::endl;

      if (recurse > 0) {
        self(recurse - 1);
      }
    };
  return recursive< void(int) >( f );
};

Здесь мы делаем популярным auto f = lambda синтаксис. Нет необходимости хранить его в std::function,

Затем мы явно приведем его к recursive<void(int)>, который связывает его в узлы и удаляет recursive<void(int)> аргумент f с фронта и выставляет подпись void(int),

Это требует, чтобы ваш лямбда recursive<void(int)> self в качестве первого параметра, и сделать рекурсию через него, но это не кажется резким. Если бы я написал это правильно, он мог бы работать с auto&& self в качестве первого параметра, но я не уверен.

recursive<?> работает на любую подпись, естественно.

живой пример

И с отложенными вызовами во внешнем цикле это все еще работает. Обратите внимание, что я избавился от этой глобальной переменной (она будет работать с ней как с глобальной переменной, просто было грязно оставлять ее в).

В C++1y мы можем исключить стирание типа &;; shared_ptr накладные расходы вы видите выше (с recursive объект, держащий shared_ptr<function<?>>). Вы должны предоставить возвращаемое значение, так как я не могу получить result_of распутать мой беспорядок

struct wrap {};

template<class R, class F>
struct recursive {
  using base_type = F;
private:
  F base;
public:

  template<class... Ts>
  R operator()(Ts&&... ts) const
  {
    return (*base)(*this, std::forward<Ts>(ts)...);
  }

  recursive(recursive const&)=default;
  recursive(recursive&&)=default;
  recursive& operator=(recursive const&)=default;
  recursive& operator=(recursive &&)=default;
  recursive() = delete;
  template<typename L>
  recursive( wrap, L&& f ):
    base( std::forward<L>(f) )
  {}
};

template<class T>using decay_t = typename std::decay<T>::type;

template<class R, class F>
recursive<R, decay_t<F>> recurse( F&& f ) { return recursive<R, decay_t<F>>(wrap{}, std::forward<F>(f)); }

Тогда немного другая реализация get_recursive_function (где я добавил состояние для развлечения):

std::function<void (int)> get_recursive_function(int amt) {
  auto f =
    [amt] (auto&& self, int count) {
      std::cout << count << std::endl;

      if (count > 0) {
        self(count - amt);
      }
    };
  return recurse<void>( std::move(f) );
};

int main() {
  auto f = get_recursive_function(2);
  f(10);
}

использование std::function в возвращаемом значении get_recursive_function необязательно - вы можете использовать auto в С ++ 1г. Все еще есть некоторые издержки по сравнению с идеальной версией (где лямбда может получить доступ к своей собственной operator()), поскольку operator() вероятно, не знает, что он вызывается рекурсивно на том же объекте, когда он вызывает self,

Было бы заманчиво позволить operator()( blah ) в теле лямбды, чтобы позволить рекурсивный вызов лямбды. Это, вероятно, сломало бы очень мало кода.

Мой текущий обходной путь, который, к сожалению, сложен:

#include <functional>
#include <iostream>
#include <queue>

volatile bool no_optimize = true;

std::queue<std::function<void (void)>> callbacks;

(Я думаю, что это Y Combinator, но я не уверен.)

std::function<void (int)> y_combinator(
        std::function<void (std::function<void (int)>, int)> almost_recursive_function
) {
    auto bound_almost_recursive_function = [almost_recursive_function] (int input) {
        y_combinator(almost_recursive_function)(input);
    };

    return [almost_recursive_function, bound_almost_recursive_function] (int input) {
        almost_recursive_function(bound_almost_recursive_function, input);
    };
}

Это базовая функция; это не вызывает себя, но аргумент это передано. Этот аргумент должен быть самой рекурсивной функцией.

std::function<void (std::function<void (int)>, int)> get_almost_recursive_function() {
    auto almost_recursive_function = (
        [] (std::function<void (int)> bound_self, int recurse) {
            std::cout << recurse << std::endl;

            if (recurse > 0) {
                callbacks.push(std::bind(bound_self, recurse - 1));
            }
        }
    );

    if (no_optimize) {
        return almost_recursive_function;
    }

    return [] (std::function<void (int)>, int) {};
}

Таким образом, искомая функция может быть сделана путем применения комбинатора к почти рекурсивной функции.

std::function<void (int)> get_recursive_function() {
    return y_combinator(get_almost_recursive_function());
}

когда main выполняется, это выводит 10, 9,..., 0, как и хотел.

int main(int, char **) {
    callbacks.push(std::bind(get_recursive_function(), 10));

    while (!callbacks.empty()) {
        callbacks.front()();
        callbacks.pop();
    }
}

Поскольку вы уже имеете дело с std::function что добавляет немного накладных расходов повсюду, вы можете добавить постоянство памяти, которое только добавит косвенность на вызывающем сайте с unique_ptr:

std::unique_ptr<std::function<void (int)>> CreateRecursiveFunction() {
    auto result = std::make_unique<std::function<void (int)>>();

    auto ptr = result.get();
    *result = [ptr] (int recurse) { // c++1y can also capture a reference to the std::function directly [&func = *result]
        std::cout << recurse << std::endl;
        if (recurse > 0) {
            (*ptr)(recurse - 1); // with c++1y func( recurse - 1 )
        }
    };
    return result;
}
Другие вопросы по тегам