Expressões lambda em C++

No C++11 e posteriores, uma expressão lambda, geralmente chamada de lambda, é um modo conveniente de definir um objeto de função anônimo (um fechamento) diretamente no local em que ele é invocado ou passado como um argumento para uma função. Em geral, lambdas são usados para encapsular algumas linhas de código passadas para algoritmos ou funções assíncronas. Este artigo define o que são as lambdas, as compara a outras técnicas de programação. Ele descreve suas vantagens e apresenta 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:

Diagram that identifies the various parts of a lambda expression.

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 com 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. exception-specification Opcional.

  5. trailing-return-type Opcional.

  6. corpo lambda.

Cláusula capture

Um lambda pode introduzir novas variáveis em seu corpo (em C++14) e 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. Variáveis que têm o prefixo de e comercial (&) são acessadas por referência e variáveis que não têm o prefixo são acessadas por valor.

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

Você pode usar um modo de captura padrão para indicar como capturar qualquer variável externa referenciada no corpo lambda: [&] significa que todas as variáveis que 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 especificar o modo oposto explicitamente para variáveis específicas. Por exemplo, se um corpo de lambda acessar a variável externa total por referência e a variável externa factor por valor, as seguintes cláusulas de captura serão equivalentes:

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

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

Se uma cláusula de captura incluir um & padrão de captura, nenhum identificador em uma captura dessa cláusula de captura poderá ter o formato &identifier. Da mesma forma, se uma cláusula de captura incluir um = padrão de captura, nenhuma captura dessa cláusula de captura poderá ter o formato =identifier. Um identificador ou this não pode aparecer mais de uma vez em uma cláusula capture. O seguinte snippet de código 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 seguido por reticências é uma expansão do pacote, conforme 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 ponteiro this para a cláusula de captura para dar acesso às funções de membro e aos membros de dados da classe de fechamento.

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

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

Ao usar a cláusula de captura, nós recomendamos que você mantenha esses pontos em mente, especialmente ao usar lambdas com multi-threading:

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

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

  • As capturas de referência introduzem uma dependência de tempo de vida, mas as capturas de valor não possuem dependências de tempo de vida. É especialmente importante quando o lambda é executado de modo assíncrono. Se você capturar um local por referência em um lambda assíncrono, esse local poderá facilmente ter ido embora quando o lambda for executado. Seu código pode causar uma violação de acesso em tempo de execução.

Captura generalizada (C++14)

No C++14, você pode introduzir e inicializar novas variáveis na cláusula de captura, sem a necessidade de que essas variáveis existam 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 movimento (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 podem capturar variáveis e aceitar parâmetros de entrada. Uma lista de parâmetros (lambda declarator na sintaxe padrão) é opcional e, na maioria dos aspectos, se parece com a lista de parâmetros de uma função.

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

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

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

Uma expressão lambda pode usar outra expressão lambda como seu argumento. Para mais informações, confira "Expressões lambda de ordem superior" no artigo Exemplos de expressões lambda.

Porque uma lista de parâmetros é opcional, você pode omitir os parênteses vazios caso não passe argumentos para a expressão lambda e seu lambda-declarator não contenha exception-specification, trailing-return-type ou mutable.

Especificação mutável

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

Especificação de exceção

É possível usar a especificação de exceção noexcept para indicar que a expressão lambda não lança nenhuma exceção. Assim como em funções regulares, o compilador Microsoft C++ gera o aviso C4297 caso uma expressão lambda declare a especificação de exceção noexcept e o corpo lambda lance uma exceção, conforme apresentado a seguir:

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

Para mais informações, confira Especificações de exceção (gerar).

Tipo de retorno

O tipo de retorno de uma expressão lambda é deduzido automaticamente. Você não precisa usar a palavra-chave auto, a menos que especifique um trailing-return-type. O trailing-return-type se parece com a parte return-type de uma função de membro ou função comum. 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.

É possível omitir a parte return-type 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 contém uma instrução de retorno, o compilador deduzirá o tipo de retorno do tipo da expressão de retorno. Caso contrário, o compilador deduzirá que o tipo retornado é void. Considere os seguintes snippets de código do 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 gerar outra expressão lambda como seu valor de retorno. Para mais informações, confira "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 permitida no corpo de uma função comum ou função de membro. O corpo de uma função comum e de uma expressão lambda pode acessar os seguintes tipos de variáveis:

  • Variáveis capturadas do escopo delimitador, conforme descrito anteriormente.

  • Parâmetros.

  • Variáveis declaradas localmente.

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

  • Qualquer variável que tenha duração de armazenamento estática, como variáveis globais.

O exemplo a seguir contém uma expressão lambda que captura explicitamente a variável n por valor e que 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 pelo valor, seu valor permanece 0 após a chamada para a expressão lambda. A especificação mutable permite que n seja modificada na lambda.

Uma expressão lambda só pode capturar variáveis que têm duração automática de armazenamento. Porém, você pode usar variáveis que tenham a duração de armazenamento estático no corpo de uma expressão lambda. O exemplo a seguir usa a função generate e uma expressão lambda para atribuir um valor para cada elemento em um objeto vector. 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 mais informações, confira generate.

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 da biblioteca C++ Standard generate_n. Essa expressão lambda atribui um elemento de um objeto vector à soma dos dois elementos anteriores. A palavra-chave mutable é usada de modo que o corpo da expressão lambda possa modificar suas cópias das variáveis externas x e y, que a expressão lambda captura por valor. Uma vez que a expressão lambda captura as variáveis originais x e y por valor, seus valores permanecem 1 depois que a lambda é executada.

// 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, confira generate_n.

Expressões lambda constexpr

Visual Studio 2017 versão 15.3 e posterior (disponível no modo /std:c++17 e posteriores): 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; }();
    }

Uma lambda será implicitamente constexpr se o seu resultado atender aos requisitos de uma função constexpr:

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

    constexpr int response = answer(10);

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

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

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

Específico da Microsoft

As lambdas não têm suporte nas seguintes entidades gerenciadas de CLR (Common Language Runtime): ref class, ref struct, value class ou value struct.

Se você está usando um modificador específico da Microsoft como __declspec, 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 em particular tem suporte por lambdas, confira o artigo sobre o modificador na seção Modificadores específicos da Microsoft.

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

Confira também

Referência da linguagem C++
Objetos de função na Biblioteca Padrão C++
Chamada de função
for_each