Expressões do Lambda em C++

Em C++11 e posteriores, uma expressão lambda — muitas vezes chamada de lambda — é uma maneira conveniente de definir um objeto de função anônimo (um fechamento) exatamente no local onde ele é invocado ou passado como um argumento para uma função. Normalmente, lambdas são usados para encapsular algumas linhas de código que são passadas para algoritmos ou funções assíncronas. Este artigo define o que são lambdas e as compara com outras técnicas de programação. Descreve as suas vantagens e fornece alguns exemplos básicos.

Partes de uma expressão lambda

Aqui está um lambda simples que é passado como o terceiro argumento para a std::sort() função:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

Esta ilustração mostra as partes da sintaxe lambda:

Diagrama que identifica as várias partes de uma expressão lambda.

O exemplo de expressão lambda é [=]() mutable throw() -> int { return x+y; } O [=] é a cláusula de captura; também conhecido como lambda-introducer na especificação C++. Os parênteses são para a lista de parâmetros. A palavra-chave mutável é opcional. throw() é a especificação de exceção opcional. -> int é o tipo de retorno à direita opcional. O corpo lambda consiste na declaração dentro das chaves encaracoladas, ou retorno x+y; Estes são explicados em mais detalhes após a imagem.

  1. cláusula capture (também conhecida como lambda-introducer na especificação C++.)

  2. Lista de parâmetros Opcional. (Também conhecido como declarador lambda)

  3. Especificação mutável Opcional.

  4. exceção-especificação Opcional.

  5. tipo de retorno à direita Opcional.

  6. corpo lambda.

Cláusula de captura

Um lambda pode introduzir novas variáveis em seu corpo (em C++14), e também pode acessar ou capturar variáveis do escopo ao redor. Um lambda começa com a cláusula de captura. Ele especifica quais variáveis são capturadas e se a captura é por valor ou por referência. As variáveis que têm o prefixo E comercial (&) são acessadas por referência e as variáveis que não o têm são acessadas por valor.

Uma cláusula de captura vazia, [ ], indica que o corpo da expressão lambda não acessa nenhuma variável no escopo de delimitação.

Você pode usar um modo de captura padrão para indicar como capturar quaisquer variáveis externas referenciadas no corpo lambda: [&] significa que todas as variáveis às quais você se refere são capturadas por referência e [=] significa que elas são capturadas por valor. Você pode usar um modo de captura padrão e, em seguida, especificar o modo oposto explicitamente para variáveis específicas. Por exemplo, se um corpo lambda acessa a variável total externa por referência e a variável factor externa por valor, as seguintes cláusulas de captura são equivalentes:

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

Somente as variáveis mencionadas no corpo lambda são capturadas quando um padrão de captura é usado.

Se uma cláusula de captura incluir um padrão de captura &, nenhum identificador em uma captura dessa cláusula de captura poderá ter a forma &identifier. Da mesma forma, se a cláusula de captura incluir um padrão =de captura, nenhuma captura dessa cláusula de captura poderá ter a forma =identifier. Um identificador ou this não pode aparecer mais de uma vez em uma cláusula de captura. O trecho de código a seguir ilustra alguns exemplos:

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

Uma captura seguida de reticências é uma expansão de pacote, como mostrado neste exemplo de modelo variádico :

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

Para usar expressões lambda no corpo de uma função de membro de classe, passe o this ponteiro para a cláusula capture para fornecer acesso às funções de membro e aos membros de dados da classe anexa.

Visual Studio 2017 versão 15.3 e posterior (disponível no /std:c++17 modo e posterior): O this ponteiro pode ser capturado por valor especificando *this na cláusula de captura. A captura por valor copia todo o fechamento para cada site de chamada onde o lambda é invocado. (Um fechamento é o objeto de função anônimo que encapsula a expressão lambda.) A captura por valor é útil quando o lambda é executado em operações paralelas ou assíncronas. É especialmente útil em determinadas arquiteturas de hardware, como o NUMA.

Para obter um exemplo que mostra como usar expressões lambda com funções de membro de classe, consulte "Exemplo: Usando uma expressão lambda em um método" em Exemplos de expressões lambda.

Ao usar a cláusula de captura, recomendamos que você tenha estes pontos em mente, especialmente quando usa lambdas com multithreading:

  • As capturas de referência podem ser usadas para modificar variáveis externas, mas as capturas de valor não podem. mutable( permite que cópias sejam modificadas, mas não originais.)

  • As capturas de referência refletem atualizações de variáveis externas, mas as capturas de valor não.

  • As capturas de referência introduzem uma dependência vitalícia, mas as capturas de valor não têm dependências vitalícias. É especialmente importante quando o lambda é executado de forma assíncrona. Se você capturar um local por referência em um lambda assíncrono, esse local poderá facilmente desaparecer quando o lambda for executado. Seu código pode causar uma violação de acesso em tempo de execução.

Captura generalizada (C++14)

Em C++14, você pode introduzir e inicializar novas variáveis na cláusula de captura, sem a necessidade de ter essas variáveis no escopo de inclusão da função lambda. A inicialização pode ser expressa como qualquer expressão arbitrária; O tipo da nova variável é deduzido do tipo produzido pela expressão. Esse recurso permite capturar variáveis somente de movimentação (como std::unique_ptr) do escopo ao redor e usá-las em um lambda.

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

Lista de parâmetros

O Lambdas pode capturar variáveis e aceitar parâmetros de entrada. Uma lista de parâmetros (declarador lambda na sintaxe Padrão) é opcional e, na maioria dos aspetos, se assemelha à lista de parâmetros de uma função.

auto y = [] (int first, int second)
{
    return first + second;
};

Em C++14, se o tipo de parâmetro for genérico, você poderá usar a auto palavra-chave como especificador de tipo. Essa palavra-chave diz ao compilador para criar o operador de chamada de função como um modelo. Cada instância de em uma lista de parâmetros é equivalente a um parâmetro de auto tipo distinto.

auto y = [] (auto first, auto second)
{
    return first + second;
};

Uma expressão lambda pode tomar outra expressão lambda como argumento. Para obter mais informações, consulte "Higher-Order Lambda Expressions" no artigo Exemplos de expressões lambda.

Como uma lista de parâmetros é opcional, você pode omitir os parênteses vazios se não passar argumentos para a expressão lambda e seu declarador lambda não contiver exceção-especificação, trailing-return-type ou mutable.

Especificação mutável

Normalmente, o operador de chamada de função de um lambda é const-by-value, mas o uso da palavra-chave cancela mutable isso. Ele não produz membros de dados mutáveis. A mutable especificação permite que o corpo de uma expressão lambda modifique variáveis que são capturadas por valor. Alguns dos exemplos mais adiante neste artigo mostram como usar mutableo .

Especificação de exceção

Você pode usar a noexcept especificação de exceção para indicar que a expressão lambda não lança exceções. Como com funções comuns, o compilador Microsoft C++ gera aviso C4297 se uma expressão lambda declara a noexcept especificação de exceção e o corpo lambda lança uma exceção, como mostrado aqui:

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

Para obter mais informações, consulte Especificações de exceção (throw).

Tipo de retorno

O tipo de retorno de uma expressão lambda é deduzido automaticamente. Você não precisa usar a palavra-chave, a auto menos que especifique um trailing-return-type. O trailing-return-type se assemelha à parte do tipo return de uma função ordinária ou função de membro. No entanto, o tipo de retorno deve seguir a lista de parâmetros e você deve incluir a palavra-chave -> trailing-return-type antes do tipo de retorno.

Você pode omitir a parte do tipo return de uma expressão lambda se o corpo lambda contiver apenas uma instrução return. Ou, se a expressão não retornar um valor. Se o corpo lambda contiver uma instrução return, o compilador deduzirá o tipo de return do tipo da expressão return. Caso contrário, o compilador deduz o tipo de retorno como void. Considere os seguintes trechos de código de exemplo que ilustram esse princípio:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

Uma expressão lambda pode produzir outra expressão lambda como seu valor de retorno. Para obter mais informações, consulte "Expressões lambda de ordem superior" em Exemplos de expressões lambda.

Corpo lambda

O corpo lambda de uma expressão lambda é uma instrução composta. Ele pode conter qualquer coisa que é permitida no corpo de uma função comum ou função de membro. O corpo de uma função comum e uma expressão lambda pode acessar esses tipos de variáveis:

  • Variáveis capturadas do escopo de inclusão, conforme descrito anteriormente.

  • Parâmetros.

  • Variáveis declaradas localmente.

  • Membros de dados de classe, quando declarados dentro de uma classe e this capturados.

  • Qualquer variável que tenha duração de armazenamento estático — por exemplo, variáveis globais.

O exemplo a seguir contém uma expressão lambda que captura explicitamente a variável n por valor e captura implicitamente a variável m por referência:

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

Como a variável n é capturada por valor, seu valor permanece 0 após a chamada para a expressão lambda. A mutable especificação permite n ser modificada dentro da lambda.

Uma expressão lambda só pode capturar variáveis que tenham duração de armazenamento automático. No entanto, você pode usar variáveis que têm duração de armazenamento estático no corpo de uma expressão lambda. O exemplo a seguir usa a generate função e uma expressão lambda para atribuir um valor a cada elemento em um vector objeto. A expressão lambda modifica a variável estática para gerar o valor do próximo elemento.

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

Para obter mais informações, consulte gerar.

O exemplo de código a seguir usa a função do exemplo anterior e adiciona um exemplo de uma expressão lambda que usa o algoritmo generate_nC++ Standard Library . Esta expressão lambda atribui um elemento de um vector objeto à soma dos dois elementos anteriores. A mutable palavra-chave é usada para que o corpo da expressão lambda possa modificar suas cópias das variáveis x externas e y, que a expressão lambda captura por valor. Como a expressão lambda captura as variáveis x originais e y por valor, seus valores permanecem 1 após a execução do lambda.

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

Para obter mais informações, consulte generate_n.

constexpr expressões lambda

Visual Studio 2017 versão 15.3 e posterior (disponível no /std:c++17 modo e posterior): Você pode declarar uma expressão lambda como constexpr (ou usá-la em uma expressão constante) quando a inicialização de cada membro de dados capturado ou introduzido é permitida dentro de uma expressão constante.

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

Um lambda é implicitamente constexpr se o seu resultado satisfaz os requisitos de uma constexpr função:

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

Se um lambda for implícita ou explicitamente constexpr, a conversão para um ponteiro de função produz uma constexpr função:

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

Específico da Microsoft

Não há suporte para lambdas nas seguintes entidades gerenciadas pelo Common Language Runtime (CLR): ref class, ref struct, value classou value struct.

Se você estiver usando um modificador específico da Microsoft, como __declspeco , poderá inseri-lo em uma expressão lambda imediatamente após o parameter-declaration-clause. Por exemplo:

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Para determinar se um modificador específico é suportado por lambdas, consulte o artigo sobre o modificador na seção modificadores específicos da Microsoft .

O Visual Studio oferece suporte à funcionalidade lambda C++11 Standard e lambdas sem monitoração de estado. Um lambda sem estado é conversível em um ponteiro de função que usa uma convenção de chamada arbitrária.

Ver também

Referência da linguagem C++
Objetos de função na biblioteca padrão do C++
Chamada de função
for_each