parameter - O que é uma expressão lambda no C++ 11?




funções lambda c++ (6)

O problema

C ++ inclui funções genéricas úteis como std::for_each e std::transform , que podem ser muito úteis. Infelizmente, eles também podem ser bastante complicados de usar, especialmente se o functor você deseja aplicar for exclusivo para a função específica.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Se você só usa f uma vez e naquele lugar específico, parece ser um exagero estar escrevendo uma classe inteira apenas para fazer algo trivial e um fora.

Em C ++ 03 você pode ser tentado a escrever algo como o seguinte, para manter o functor local:

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

no entanto isso não é permitido, f não pode ser passado para uma função de template em C ++ 03.

A nova solução

C ++ 11 introduz lambdas permite que você escreva um functor anônimo inline para substituir a struct f . Para pequenos exemplos simples, isso pode ser mais limpo de ler (mantém tudo em um só lugar) e potencialmente mais simples de manter, por exemplo, da forma mais simples:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Funções lambda são apenas açúcar sintático para functores anônimos.

Tipos de retorno

Em casos simples, o tipo de retorno do lambda é deduzido para você, por exemplo:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

no entanto, quando você começa a escrever lambdas mais complexos, você encontrará rapidamente casos em que o tipo de retorno não pode ser deduzido pelo compilador, por exemplo:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Para resolver isso, você tem permissão para especificar explicitamente um tipo de retorno para uma função lambda, usando -> T :

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Variáveis ​​"Capturando"

Até agora não usamos nada além do que foi passado para o lambda dentro dele, mas também podemos usar outras variáveis, dentro do lambda. Se você quiser acessar outras variáveis, você pode usar a cláusula de captura (o [] da expressão), que até agora não foi utilizada nesses exemplos, por exemplo:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Você pode capturar por referência e valor, que você pode especificar usando & e = respectivamente:

  • [&epsilon] captura por referência
  • [&] captura todas as variáveis ​​usadas no lambda por referência
  • [=] captura todas as variáveis ​​usadas no lambda por valor
  • [&, epsilon] captura variáveis ​​como com [&], mas epsilon por valor
  • [=, &epsilon] captura variáveis ​​como com [=], mas epsilon por referência

O operator() gerado operator() é const por padrão, com a implicação de que as capturas serão const quando você as acessa por padrão. Isso tem o efeito de que cada chamada com a mesma entrada produziria o mesmo resultado, no entanto, você pode marcar o lambda como mutable para solicitar que o operator() que é produzido não seja const .

O que é uma expressão lambda no C ++ 11? Quando eu usaria um? Que tipo de problema eles resolvem que não era possível antes de sua introdução?

Alguns exemplos e casos de uso seriam úteis.


O que é uma função lambda?

O conceito C ++ de uma função lambda se origina no cálculo lambda e na programação funcional. Um lambda é uma função sem nome que é útil (na programação atual, não na teoria) para pequenos fragmentos de código que são impossíveis de serem reutilizados e que não valem a pena nomear.

Em C ++, uma função lambda é definida assim

[]() { } // barebone lambda

ou em toda a sua glória

[]() mutable -> T { } // T is the return type, still lacking throw()

[] é a lista de captura, () a lista de argumentos e {} o corpo da função.

A lista de captura

A lista de captura define o que, do lado de fora do lambda, deve estar disponível dentro do corpo da função e como. Pode ser:

  1. um valor: [x]
  2. uma referência [& x]
  3. qualquer variável atualmente no escopo por referência [&]
  4. mesmo que 3, mas por valor [=]

Você pode misturar qualquer um dos itens acima em uma lista separada por vírgula [x, &y] .

A lista de argumentos

A lista de argumentos é a mesma que em qualquer outra função C ++.

O corpo da função

O código que será executado quando o lambda for realmente chamado.

Dedução do tipo de retorno

Se um lambda tiver apenas uma instrução de retorno, o tipo de retorno poderá ser omitido e terá o tipo implícito de decltype(return_statement) .

Mutável

Se um lambda é marcado mutável (por exemplo, []() mutable { } ), é permitido alterar os valores que foram capturados por valor.

Casos de uso

A biblioteca definida pelo padrão ISO se beneficia muito de lambdas e aumenta a usabilidade de várias barras, já que agora os usuários não precisam confundir seu código com pequenos functores em algum escopo acessível.

C ++ 14

Em C ++, 14 lambdas foram alargados por várias propostas.

Capturas inicializadas do Lambda

Um elemento da lista de captura agora pode ser inicializado com = . Isso permite renomear variáveis ​​e capturar movendo. Um exemplo retirado do padrão:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

e um tirado da Wikipedia mostrando como capturar com std::move :

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Lambdas Genérico

Lambdas agora pode ser genérico ( auto seria equivalente a T aqui se T fosse um argumento de modelo de tipo em algum lugar no escopo ao redor):

auto lambda = [](auto x, auto y) {return x + y;};

Dedução de tipo de retorno melhorada

O C ++ 14 permite tipos de retorno deduzidos para cada função e não o restringe a funções da return expression; formulário return expression; . Isso também é estendido para lambdas.


Bem, um uso prático que descobri é reduzir o código da placa da caldeira. Por exemplo:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

Sem lambda, você pode precisar fazer algo para diferentes casos de bsize . É claro que você poderia criar uma função, mas e se você quiser limitar o uso dentro do escopo da função de usuário da alma? a natureza do lambda cumpre este requisito e eu o uso para esse caso.



Uma função lambda é uma função anônima que você cria na linha. Ele pode capturar variáveis ​​como algumas explicaram (por exemplo, http://www.stroustrup.com/C++11FAQ.html#lambda ), mas há algumas limitações. Por exemplo, se houver uma interface de retorno de chamada como essa,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

você pode escrever uma função no local para usá-la como a que foi passada abaixo:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Mas você não pode fazer isso:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

por causa das limitações no padrão C ++ 11. Se você quiser usar capturas, você tem que confiar na biblioteca e

#include <functional> 

(ou alguma outra biblioteca STL como o algoritmo para obtê-lo indiretamente) e, em seguida, trabalhar com std :: function em vez de passar funções normais como parâmetros como este:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

Respostas

P: O que é uma expressão lambda no C ++ 11?

R: Sob o capô, é o objeto de uma classe autogerada com o operador de sobrecarga () const . Tal objeto é chamado fechamento e criado pelo compilador. Este conceito de 'fechamento' está próximo do conceito de ligação do C ++ 11. Mas os lambdas normalmente geram um código melhor. E as chamadas através de encerramentos permitem um inline completo.

Q: Quando eu usaria um?

R: Para definir "lógica simples e pequena" e peça ao compilador que execute a geração da pergunta anterior. Você dá ao compilador algumas expressões que você quer que estejam dentro de operator (). Todo o outro compilador de material irá gerar para você.

P: Que tipo de problema eles resolvem que não era possível antes de sua introdução?

R: É algum tipo de sintaxe de açúcar, como operadores sobrecarregando em vez de funções para adicionar custom , operações de subrtact ... Mas salve mais linhas de código desnecessário para agrupar de 1 a 3 linhas de lógica real em algumas classes e etc. Alguns engenheiros pensam que, se o número de linhas for menor, haverá menos chance de cometer erros (acho que também)

Exemplo de uso

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Extras sobre lambdas, não cobertos pela pergunta. Ignore esta seção se você não estiver interessado

1. Valores capturados. O que você pode capturar

1.1. Você pode fazer referência a uma variável com duração de armazenamento estático em lambdas. Todos eles são capturados.

1.2. Você pode usar lambda para valores de captura "por valor". Nesse caso, as variáveis ​​capturadas serão copiadas para o objeto de função (encerramento).

[captureVar1,captureVar2](int arg1){}

1.3. Você pode capturar ser referência. & - neste contexto significa referência, não ponteiros.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Existe notação para capturar todos os valores não-estáticos por valor, ou por referência

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Existe notação para capturar todos os valores não-estáticos por valor, ou por referência e especificar smth. Mais. Exemplos: Capture todos os valores não estáticos por valor, mas por captura de referência Param2

[=,&Param2](int arg1){} 

Capture todos os vars não estáticos por referência, mas por captura de valor Param2

[&,Param2](int arg1){} 

2. Retorno do tipo dedução

2.1. O tipo de retorno lambda pode ser deduzido se lambda for uma expressão. Ou você pode especificá-lo explicitamente.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Se lambda tiver mais de uma expressão, o tipo de retorno deverá ser especificado por meio do tipo de retorno à direita. Além disso, uma sintaxe semelhante pode ser aplicada a funções automáticas e funções de membro

3. Valores capturados. O que você não pode capturar

3.1. Você pode capturar apenas vars locais, não variável de membro do objeto.

4. vencções

4.1 !! Lambda não é um ponteiro de função e não é uma função anônima, mas lambdas sem captura podem ser implicitamente convertidos em um ponteiro de função.

ps

  1. Mais informações sobre a gramática lambda podem ser encontradas em Esboço de Trabalho para Linguagem de Programação C ++ # 337, 2012-01-16, 5.1.2. Expressões Lambda, p.

  2. Em C ++ 14 o recurso extra que foi nomeado como "captura de init" foi adicionado. Permite executar arbitrariamente declaração de membros de dados de fechamento:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};




c++-faq