Expresiones lambda en C++

En C++11 y versiones posteriores, una expresión lambda, a menudo denominada lambda, es una manera cómoda de definir un objeto de función anónimo (un cierre) justo en la ubicación donde se invoca o se pasa como argumento a una función. Normalmente, las expresiones lambda se usan para encapsular unas líneas de código que se pasan a algoritmos o métodos asincrónicos. En este artículo se definen las expresiones lambda y se comparan con otras técnicas de programación. Describe sus ventajas y proporciona algunos ejemplos básicos.

Partes de una expresión lambda

Esta es una expresión lambda simple que se pasa como tercer argumento a la std::sort() función:

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

En esta ilustración se muestran las partes de la sintaxis lambda:

Diagram that identifies the various parts of a lambda expression.

El ejemplo de expresión lambda es [=]() mutable throw() -> int { return x+y; } [=] es la cláusula capture; también conocido como lambda-introducer en la especificación de C++. Los paréntesis son para la lista de parámetros. La palabra clave mutable es opcional. throw() es la especificación de excepción opcional. -> int es el tipo de valor devuelto final opcional. El cuerpo lambda consta de la instrucción dentro de las llaves o devuelve x+y; Estos se explican con más detalle después de la imagen.

  1. cláusula de captura (también conocida como iniciador de expresión lambda en la especificación de C++)

  2. lista de parámetros Opcional. (también conocida como declarador de expresión lambda)

  3. especificación mutable Opcional.

  4. especificación de excepción Opcional.

  5. tipo de valor devuelto final Opcional.

  6. cuerpo lambda.

Cláusula de captura

Una expresión lambda puede introducir nuevas variables en su cuerpo (en C++14) y también puede acceder a variables o capturarlas del ámbito circundante. Una expresión lambda comienza con la cláusula de captura. Especifica qué variables se capturan y si la captura es por valor o por referencia. A las variables que tienen como prefijo y comercial (&) se accede mediante referencia y a las variables que no tienen el prefijo se accede por valor.

Una cláusula de captura vacía, [ ], indica que el cuerpo de la expresión lambda no tiene acceso a ninguna variable en el ámbito de inclusión.

Puede usar un modo de captura predeterminado para indicar cómo capturar las variables externas a las que se hace referencia en el cuerpo lambda: [&] significa que todas las variables a las que hace referencia se capturan por referencia y [=] significa que se capturan por valor. Puede usar un modo de captura predeterminado y, después, especificar el modo opuesto de forma explícita para unas variables específicas. Por ejemplo, si el cuerpo de una expresión lambda accede a la variable externa total por referencia y a la variable externa factor por valor, las siguientes cláusulas capture serán equivalentes:

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

Solo se capturan las variables que se mencionan en el cuerpo lambda cuando se usa capture-default.

Si una cláusula de captura incluye una cláusula capture-default &, ningún identificador de una captura de esa cláusula de captura puede tener el formato &identifier. Del mismo modo, si la cláusula de captura incluye una cláusula capture-default =, ninguna captura de esa cláusula de captura puede tener el formato =identifier. Un identificador o this no puede aparecer más de una vez en una cláusula de captura. En el fragmento de código siguiente se muestran algunos ejemplos:

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
}

Una captura seguida de puntos suspensivos es una expansión de paquetes, como se muestra en este ejemplo de: plantilla variádica

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

Para usar expresiones lambda en el cuerpo de una función miembro de clase, pase el puntero this a la cláusula de captura para proporcionar acceso a las funciones miembro y a los miembros de datos de la clase contenedora.

Visual Studio 2017 versión 15.3 y posteriores (disponible en modo /std:c++17 y versiones posteriores): el puntero this se puede capturar por valor especificando *this en la cláusula de captura. La captura por valor copia el cierre completo en cada sitio de llamada donde se invoca la expresión lambda. (Un cierre es el objeto de función anónima que encapsula la expresión lambda). La captura por valor es útil cuando la expresión lambda se ejecuta en operaciones paralelas o asincrónicas. Es especialmente útil en determinadas arquitecturas de hardware, como NUMA.

Para obtener un ejemplo en el que se muestra cómo usar expresiones lambda con funciones miembro de clase, vea "Ejemplo: Uso de una expresión lambda en un método" en Ejemplos de expresiones lambda.

Al usar la cláusula capture, se recomienda tener en cuenta estos puntos, especialmente cuando se usan expresiones lambda con multiprocesamiento:

  • Se pueden usar capturas por referencia para modificar variables externas, pero no se pueden usar capturas por valor. (mutable permite modificar copias, pero no originales)

  • Las capturas por referencia reflejan actualizaciones en variables externas, pero las capturas por valor no.

  • Las capturas por referencia presentan una dependencia de la duración, pero las capturas por valor no tienen ninguna dependencia de la duración. Esto es muy importante cuando la expresión lambda se inicia de forma asincrónica. Si captura un valor local por referencia en una expresión lambda asincrónica, esa configuración local podría haber desaparecido fácilmente en el momento en que se ejecuta la expresión lambda. El código podría provocar una infracción de acceso en tiempo de ejecución.

Captura generalizada (C++14)

En C++14, puede introducir e inicializar nuevas variables en la cláusula de captura, sin necesidad de que esas variables existan en el ámbito envolvente de la función lambda. La inicialización se puede expresar como cualquier expresión arbitraria; el tipo de la variable nueva se deduce del tipo producido por la expresión. Esta característica le permite capturar variables de solo movimiento (como std::unique_ptr) del ámbito circundante y usarlas en una expresión lambda.

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

Lista de parámetros

Las expresiones lambda pueden capturar variables y aceptar parámetros de entrada. La lista de parámetros (declarador de expresión lambda en la sintaxis estándar) es opcional y, en la mayoría de los aspectos, es similar a la lista de parámetros de una función.

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

En C++14, si el tipo de parámetro es genérico, puede usar la palabra clave auto como especificador de tipo. Esto indica al compilador que debe crear el operador de llamada de función como plantilla. Cada instancia de auto en una lista de parámetros es equivalente a un parámetro de tipo distinto.

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

Una expresión lambda puede tomar otra expresión lambda como argumento. Para obtener más información, vea "Expresiones lambda de orden superior" en el artículo ejemplos de expresiones lambda.

Dado que una lista de parámetros es opcional, puede omitir los paréntesis vacíos si no pasa argumentos a la expresión lambda y su declarador lambda no contiene una especificación de excepción , trailing-return-type o mutable.

Especificación mutable

Normalmente, el operador de llamada de función de una expresión lambda es const-by-value, pero el uso de la palabra clave mutable lo cancela. No genera miembros de datos mutables. La especificación mutable permite al cuerpo de una expresión lambda modificar las variables que se capturan por valor. Algunos de los ejemplos que se incluyen más adelante en este artículo muestran el uso de mutable.

Especificación de la excepción

Puede usar la especificación de excepciónnoexceptpara indicar que la expresión lambda no produce ninguna excepción. Al igual que con las funciones normales, el compilador de Microsoft C++ genera una advertencia C4297 si una expresión lambda declara la especificación de excepción noexcept y el cuerpo lambda produce una excepción, como se muestra aquí:

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

Para obtener más información, vea Especificaciones de excepción (throw).

Tipo de valor devuelto

El tipo de valor devuelto de una expresión lambda se deduce automáticamente. No tiene que usar la palabra clave auto a menos que especifique un trailing-return-type. El trailing-return-type es similar a la parte de tipo de valor devuelto de una función normal o una función miembro. Pero el tipo de valor devuelto debe seguir la lista de parámetros y debe incluir la palabra clave trailing-return-type -> antes del tipo de valor devuelto.

Puede omitir la parte de tipo de valor devuelto de una expresión lambda si el cuerpo lambda contiene solo una instrucción return. O bien, si la expresión no devuelve un valor. Si el cuerpo de la expresión lambda contiene una instrucción return, el compilador deduce el tipo de valor devuelto del tipo de expresión return. En caso contrario, el compilador deduce que el tipo de valor devuelto es void. Vea los fragmentos de código de ejemplo siguientes que muestran este principio:

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

Una expresión lambda puede generar otra expresión lambda como valor devuelto. Para obtener más información, vea "Expresiones lambda de orden superior" en Ejemplos de expresiones lambda.

Cuerpo lambda

El cuerpo lambda de una expresión lambda es una instrucción compuesta. Puede contener todo lo que se permite en el cuerpo de una función normal o una función miembro. El cuerpo de una función normal y de una expresión lambda puede tener acceso a estos tipos de variables:

  • variables capturadas en el ámbito de inclusión, tal como se describió anteriormente.

  • Parámetros.

  • Variables declaradas localmente.

  • Miembros de datos de la clase, cuando se declara dentro de una clase y se captura this.

  • Cualquier variable que tenga duración de almacenamiento estática, por ejemplo, variables globales.

El ejemplo siguiente contiene una expresión lambda que captura explícitamente la variable n por valor y captura implícitamente la variable m por referencia:

// 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 la variable n se captura por valor, el valor sigue siendo 0 después de la llamada a la expresión lambda. La especificación mutable permite modificar n dentro de la expresión lambda.

Una expresión lambda solo puede capturar variables que tengan una duración de almacenamiento automática. Sin embargo, puede usar variables que tengan una duración de almacenamiento estática en el cuerpo de una expresión lambda. En el ejemplo siguiente se utiliza la función generate y una expresión lambda para asignar un valor a cada elemento de un objeto vector. La expresión lambda modifica la variable estática para generar el valor del elemento siguiente.

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 obtener más información, vea generar.

En el ejemplo de código siguiente se usa la función del ejemplo anterior y se agrega un ejemplo de una expresión lambda que usa el algoritmo de biblioteca estándar C++ generate_n. Esta expresión lambda asigna un elemento de un objeto vector a la suma de los dos elementos anteriores. Se usa la palabra clave mutable de modo que el cuerpo de la expresión lambda pueda modificar sus copias de las variables externas x e y, que la expresión lambda captura por valor. Como la expresión lambda captura las variables originales x e y por valor, sus valores siguen siendo 1 después de la ejecución de la expresión.

// 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 obtener más información, consulte generate_n.

constexpr expresiones lambda

Visual Studio 2017 versión 15.3 y posteriores (disponible en modo /std:c++17 y versiones posteriores): puede declarar una expresión lambda como constexpr (o usarla en una expresión constante) cuando se permite la inicialización de cada miembro de datos capturado o introducido dentro de una expresión constante.

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

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

Una expresión lambda es implícitamente constexpr si su resultado cumple los requisitos de una funciónconstexpr:

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

    constexpr int response = answer(10);

Si una expresión lambda es implícita o explícitamente constexpr, la conversión a un puntero de función genera una funciónconstexpr:

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

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

Específico de Microsoft

Las expresiones lambda no se admite en las entidades administradas siguientes de Common Language Runtime (CLR): ref class,ref struct,value class, o value struct.

Si usa un modificador específico de Microsoft como __declspec, puede insertarlo en una expresión lambda inmediatamente después de la parameter-declaration-clause. Por ejemplo:

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

Para determinar si un modificador es compatible con las expresiones lambda, consulte el artículo correspondiente en la sección Modificadores específicos de Microsoft de la documentación.

Visual Studio admite la funcionalidad lambda estándar de C++11 y lambdas sin estado. Una expresión lambda sin estado se puede convertir en un puntero de función que usa una convención de llamada arbitraria.

Consulte también

Referencia del lenguaje C++
Objetos de función en la biblioteca estándar de C++
Llamada a función
for_each