Funções lambda recursivas em C ++ 11


143

Eu sou novo no C ++ 11. Estou escrevendo a seguinte função lambda recursiva, mas ela não é compilada.

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;
}

Erro de compilação:

Para obter mais informações, consulte a página de suporte do Microsoft Visual Studio.

sum.cpp: Na função lambda: sum.cpp: 18: 36: erro: ' ((<lambda(int, int)>*)this)-><lambda(int, int)>::sum' não pode ser usado como uma função

versão gcc

gcc versão 4.5.0 20091231 (experimental) (GCC)

Mas se eu alterar a declaração sum()como abaixo, ela funcionará:

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

Alguém poderia por favor esclarecer isso?


Poderia ser declarações estáticas vs implicitamente dinâmicas?
Hamish Grubijan

3
O que a mutablepalavra - chave está fazendo lá?
Saúde e hth. - Alf

A captura de variáveis ​​com duração de armazenamento não automática não é permitida. Você deve fazê-lo desta maneira: chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow

Apenas um FYI, no seu segundo trecho de código, seu lambda é muito detalhado, considere esta alteração:std::function<int(int,int)> sum = [&](int a, int b) {
armanali 22/07

Respostas:


189

Pense na diferença entre a versão automática e a versão do tipo totalmente especificada. A palavra - chave auto infere seu tipo do que quer que seja inicializado, mas o que você está inicializando precisa saber qual é o seu tipo (nesse caso, o fechamento lambda precisa saber os tipos que está capturando). Algo como um problema de galinha e ovo.

Por outro lado, o tipo de um objeto de função totalmente especificado não precisa "saber" nada sobre o que está sendo atribuído a ele e, portanto, o fechamento do lambda também pode ser totalmente informado sobre os tipos que está capturando.

Considere esta pequena modificação do seu código e pode fazer mais sentido:

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

Obviamente, isso não funcionaria com auto . As funções lambda recursivas funcionam perfeitamente bem (pelo menos elas funcionam no MSVC, onde tenho experiência com elas), mas elas não são realmente compatíveis com a inferência de tipo.


3
Eu não concordo com isso. O tipo de lambda é bem conhecido assim que o corpo da função é inserido - não há razão para que não deva ser deduzido até então.
Puppy

16
@DeadMG, mas a especificação proíbe a referência à autovariável no inicializador. o tipo da variável automática ainda não é conhecido quando o inicializador está sendo processado.
Johannes Schaub - litb 23/06

1
Querendo saber por que isso não está marcado como 'resposta', e que o Python é classificado como 'Resposta' ?!
Ajay

1
@ Filhote: No entanto, no caso de uma captura implícita, por questões de eficiência, apenas as variáveis ​​referenciadas são realmente capturadas; portanto, o corpo deve ser analisado.
kec

Existe uma interpretação válida para sumoutra std::function<int(int, int)>ou a especificação C ++ não se deu ao trabalho de inferir isso?
Mateen Ulhaq

79

O truque é alimentar a implementação lambda como um parâmetro , não por captura.

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

Todos os problemas em ciência da computação podem ser resolvidos por outro nível de indireção . Encontrei esse truque fácil pela primeira vez em http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/

Ele não requer C ++ 14, enquanto a questão é em C ++ 11, mas talvez interessante para a maioria.

Ir via std::functiontambém é possível, mas pode resultar em código mais lento. Mas não sempre. Dê uma olhada nas respostas para std :: function vs template


Isso não é apenas uma peculiaridade do C ++, ele é mapeado diretamente para a matemática do cálculo lambda. Da Wikipedia :

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
Isso parece muito pior do que usar explicitamente function<>. Não vejo por que alguém preferiria isso. Edit: É aparentemente mais rápido.
21418 Timmym

17
este é muito melhor do que std :: função por 3 motivos: não requer tipo de apagamento ou a alocação de memória, pode ser constexpr e funciona corretamente com auto (templated) Parâmetros / tipo de retorno
Ivan Sanz-Carasa

3
Presumivelmente, essa solução também tem a vantagem de ser copiável sem que a referência std :: function saia do escopo?
Uri Granta

3
Hm, ao tentar, o GCC 8.1 (linux) reclamou: error: use of ‘[...]’ before deduction of ‘auto’- necessário especificar explicitamente o tipo de retorno (por outro lado, não precisava de mutação).
Aconcagua

@Aconcagua mesmo aqui com o Xcode10 e defini o padrão C ++ para 17 pares #
IceFire

39

Com o C ++ 14, agora é muito fácil criar um lambda recursivo eficiente sem a necessidade de sobrecarga adicional std::function, em apenas algumas linhas de código (com uma pequena edição do original para impedir que o usuário tire uma cópia acidental ):

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

com o qual sua sumtentativa original se torna:

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

No C ++ 17, com o CTAD, podemos adicionar um guia de dedução:

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

O que evita a necessidade da função auxiliar. Podemos apenas escrever y_combinator{[](auto self, ...){...}}diretamente.


No C ++ 20, com o CTAD para agregados, o guia de dedução não será necessário.


Isso é ótimo, mas pode-se considerar em std::forward<decltype(sum)>(sum)vez da sumúltima linha.
Johan Lundberg

@Johan Não, só há uma operator()então não há nada a ganhar com o encaminhamentosum
Barry

Ah, é verdade. Não está acostumado a usar referências de encaminhamento sem encaminhamento.
Johan Lundberg 30/08

O combinador Y certamente é o caminho a percorrer. Mas você realmente deve adicionar uma não constsobrecarga, caso o objeto de função fornecido tenha um não constoperador de chamada. E use SFINAE e calculado noexceptpara ambos. Além disso, não há mais necessidade da função maker no C ++ 17.
Deduplicator

2
@minex Sim, auto sumcopia ... mas copia a reference_wrapper, que é a mesma coisa que tirar uma referência. Fazer isso uma vez na implementação significa que nenhum dos usos será copiado acidentalmente.
Barry

22

Eu tenho outra solução, mas trabalho apenas com lambdas sem estado:

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

O truque aqui é que os lambdas podem acessar variáveis ​​estáticas e você pode converter as sem estado em ponteiro de função.

Você pode usá-lo com lambdas padrão:

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

Seu trabalho no GCC 4.7


3
Isso deve ter um desempenho melhor do que a função std ::, portanto, +1 para a alternativa. Mas, realmente, neste momento, eu me pergunto se usar lambdas é a melhor opção;)
Antoine

Se você tem um lambda sem estado, também pode torná-lo uma função completa.
Timmmm

1
@ Timmmm Mas, em seguida, você vaza parte da implementação para a palavra externa, normalmente as lambdas estão intimamente ligadas à função pai (mesmo quando não há capturas). Se não foi esse o caso, você não deve usar lambdas em primeiro lugar e usar funções normais de functors.
Yankes

10

Você pode fazer uma função lambda chamar-se recursivamente. A única coisa que você precisa fazer é referenciá-lo através de um wrapper de função para que o compilador saiba que é o tipo de retorno e argumento (você não pode capturar uma variável - a própria lambda - que ainda não foi definida) .

  function<int (int)> f;

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

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

Tenha muito cuidado para não ficar fora do escopo do invólucro f.


3
Mas, isso é idêntico à resposta aceita e pode ter uma penalidade pelo uso da função std.
Johan Lundberg 29/11

9

Para tornar o lambda recursivo sem usar classes e funções externas ( std::functioncombinador como ponto fixo), pode-se usar a seguinte construção no C ++ 14 ( exemplo ao vivo ):

#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}}});
}

impressões:

1
 2
  8
 3
  5
   7
  6
 4

Observe que o tipo de resultado lambda deve ser especificado explicitamente.


6

Fiz um benchmark comparando uma função recursiva versus uma função lambda recursiva usando o std::function<>método de captura. Com as otimizações completas ativadas na versão 4.1 do clang, a versão lambda ficou significativamente mais lenta.

#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);
}

Produz resultados:

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

(Nota: também confirmei com uma versão que utilizou as entradas do cin, para eliminar a avaliação do tempo de compilação)

Clang também produz um aviso do compilador:

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

O que é esperado e seguro, mas deve ser observado.

É ótimo ter uma solução em nossos cintos de ferramentas, mas acho que a linguagem precisará de uma maneira melhor de lidar com esse caso, se o desempenho for comparável aos métodos atuais.

Nota:

Como comentou um comentarista, parece que a versão mais recente do VC ++ encontrou uma maneira de otimizar isso até o ponto de desempenho igual. Talvez não precisemos de uma maneira melhor de lidar com isso, afinal (exceto o açúcar sintático).

Além disso, como algumas outras postagens de SO destacaram nas últimas semanas, o desempenho em std::function<>si pode ser a causa da função de desaceleração versus chamada diretamente, pelo menos quando a captura lambda é muito grande para caber em alguns std::functionusos de espaço otimizado para bibliotecas para pequenos functores (Eu acho que meio que como as várias otimizações de cadeia curta?).


2
-1. Observe que a única razão pela qual a versão "lambda" demora mais é porque você a vincula a uma função std ::, que faz o operador () chamar uma chamada virtual, e isso obviamente levaria mais tempo. Além disso, seu código, no modo de lançamento do VS2012, levou aproximadamente a mesma quantidade de tempo nos dois casos.
Yam Marcovic 27/02

@YamMarcovic What? Atualmente, essa é a única maneira conhecida de escrever uma lambda recursiva (esse foi o ponto do exemplo). Estou muito satisfeito em saber que o VS2012 encontrou uma maneira de otimizar esse caso de uso (embora tenha havido mais desenvolvimentos sobre esse tópico recentemente, aparentemente, se meu lambda tivesse capturado mais, não caberia na função std :: small- otimizações de função de memória ou outros enfeites).
27613 mmocny

2
Reconhecido. Eu entendi mal o seu post. +1 então. Gah, só pode ser votado se você editar esta resposta. Então, você poderia enfatizar um pouco mais, como no comentário?
Yam Marcovic 27/02

1
@YamMarcovic Feito. Agradeço sua disposição em fornecer feedback e refiná-lo quando necessário. +1 para você, bom senhor.
mmocny

0 tempo normalmente significa "toda a operação foi otimizada". Receber a entrada de cin não faz nada se o compilador provar que você não faz nada com o resultado do seu cálculo.
Yakk - Adam Nevraumont

1

Essa é uma implementação um pouco mais simples do operador de ponto de fixação, que torna um pouco mais óbvio exatamente o que está acontecendo.

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

Eu acho que você poderia melhorar sua resposta (desempenho) se substituir std::functionpelo ponteiro de função (de núcleos, ele só funcionará com função normal e lambdas sem estado). Btw fib_nonrdeve aceitar fixpoint<int,int>, se você usar o std::functionseu exigir criação de nova cópia de *this.
Yankes

1

Aqui está uma versão refinada da solução combinadora Y com base em uma proposta pela @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)}; };

Para usar isso, pode-se fazer o seguinte

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

É semelhante à let recpalavra - chave no OCaml, embora não seja a mesma.


0

C ++ 14: Aqui está um conjunto genérico de lambdas recursivo, sem estado / sem captura, sem captura de estado que gera todos os números de 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);

Se bem entendi, está usando a solução combinadora Y

E aqui está a versão da soma (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

Aqui está a resposta final para o OP. De qualquer forma, o Visual Studio 2010 não oferece suporte à captura de variáveis ​​globais. E você não precisa capturá-los porque a variável global é acessível globalmente por define. A resposta a seguir usa a variável local.

#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;
}

É possível fazer com que este compilador de respostas seja agnóstico?
rayryeng 8/04

-2

Você está tentando capturar uma variável (soma) que está no meio da definição. Isso não pode ser bom.

Eu não acho que lambdas C ++ 0x verdadeiramente auto-recursivas sejam possíveis. Você deve conseguir capturar outras lambdas, no entanto.


3
mas funciona se a declaração da soma for alterada de 'auto' para std :: function <int (int, int)> sem alterar a lista de capturas.
weima 14/01/10

Porque não é mais um lambda então, mas uma função que pode ser usada no lugar do lambda?
Hamish Grubijan

-2

Essa resposta é inferior à de Yankes, mas ainda assim, aqui está:

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);

Eu acho que você deveria evitar reinterpret_cast. Provavelmente, a melhor maneira no seu caso é criar alguma estrutura que substitua dp_type. Deveria ter campo fp_type, pode ser construído fp_typee ter operador ()com argumentos como fp_type. Isso estará próximo, std::functionmas permitirá o argumento de auto-referência.
Yankes

Eu queria postar um exemplo mínimo, sem uma estrutura, fique à vontade para editar minha resposta e fornecer uma solução mais completa. A structtambém adicionaria um nível adicional de indireção. O exemplo funciona e o elenco é compatível com os padrões, não sei para que serve -1.
user1095108

não, struct funcionará apenas como contêiner para ponteiro e será passado como valor. Isso não será mais indireto ou indireto do que ponteiro. E -1eu não sabia quem o entregaria, mas acho que é porque reinterpret_castdeveria ser usado como último recurso.
Yankes 27/02

O castsupostamente está garantido para o trabalho pela c ++ 11 standard. Usar um struct, aos meus olhos, poderia derrotar o uso de um objeto lambda. Afinal, o que structvocê propõe é um functor, utilizando um objeto lambda.
user1095108

Veja a solução @Pseudonym, remova apenas std::functione você terá algo parecido com o que eu tinha em mente. Provavelmente, isso terá desempenho semelhante à sua solução.
Yankes

-3

Você precisa de um combinador de ponto fixo. Veja isso .

ou observe o seguinte código:

//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;
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.