Sintaxis de las expresiones lambda

En este artículo se demuestra la sintaxis y los elementos estructurales de las expresiones lambda. Consulte la descripción de las expresiones lambda en Expresiones lambda en C++.

Gramática de expresiones lambda

En la definición siguiente de la norma ISO C++11 estándar se muestra la gramática de una expresión lambda (los elementos marcados con el subíndice opt son opcionales).

        lambda-introducer lambda-declaratoropt compound-statement

Estos componentes de sintaxis se desglosan aún más:

lambda-introducer:
        [ lambda-captureopt ]
lambda-capture:
        capture-default
        capture-list
        capture-default , capture-list
capture-default:
        &
        =
capture-list:
        capture ...opt
        capture-list , capture ...opt
capture:
        identifier
        & identifier
        this
lambda-declarator:
        ( parameter-declaration-clause ) mutableopt
                exception-specificationopt attribute-specifier-seqopt trailing-return-typeopt

Visual Studio la sintaxis de expresiones lambda y las funciones del estándar C++11, con las siguientes excepciones:

  • Al igual que el resto de clases, las expresiones lambda no obtienen constructores de movimiento ni operadores de asignación de movimiento generados automáticamente. Para obtener más información sobre la compatibilidad con los comportamientos de referencia a un valor R, consulte la sección “Referencias de valores R” de Compatibilidad con las características de C++11 (C++ moderno).

  • El elemento attribute-specifier-seq no se admite en esta versión.

Visual Studio incluye estas características además de la función de expresiones lambda del estándar C++11:

  • Las expresiones lambda sin estado, que se pueden convertir a punteros de función con convenciones de llamada arbitrarias.

  • Tipos de valores devueltos deducidos automáticamente para cuerpos lambda que son más complicados que { return expression; }, si todas las instrucciones de valor devuelto tienen el mismo tipo (esta función forma parte del estándar C++14 propuesto).

Propiedades de expresiones lambda

En esta ilustración se asigna la gramática al ejemplo:

Structural elements of a lambda expression

  1. lambda-introducer (también se conoce como cláusula de captura)

  2. declarador lambda (también se conoce como lista de parámetros)

  3. mutable (también se conoce como especificación mutable)

  4. exception-specification (también se conoce como especificación de excepción)

  5. trailing-return-type (también se conoce como tipo de valor devuelto)

  6. compound-statement (también se conoce como cuerpo de lambda)

Cláusula capture

Una expresión lambda es básicamente una clase, un constructor y un operador de llamada a función. Igual que cuando se define una clase, en una expresión lambda es necesario decidir si el objeto resultante captura variables por valor, por referencia o si no captura ninguna variable. Si una expresión lambda necesita acceder a variables locales y a parámetros de función, es necesario capturarlos. La cláusula de captura (lambda-introducer en la sintaxis estándar) especifica si el cuerpo de la expresión lambda puede acceder a variables en el ámbito de inclusión por valor o por referencia. A las variables que tienen como prefijo una 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 el modo de captura predeterminado (capture-default en la sintaxis estándar) para capturar variables sin especificar, ya sea por valor o por referencia. Puede especificar el modo de captura predeterminado mediante & o = como el primer elemento de la cláusula de captura. El elemento & indica al cuerpo de la expresión lambda que acceda a variables sin especificar por referencia. El elemento = indica al cuerpo de la expresión lambda que acceda a variables sin especificar por valor. 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]
[factor, &]
[=, &total]
[&total, =]

Un error frecuente sobre capture-default es que se capturan todas las variables del ámbito, independientemente de si se usan en la expresión lambda o no. Este no es el caso: solo se capturan las variables que se mencionan en la expresión lambda cuando se usa capture-default.

Si una cláusula de captura incluye capture-default &, ningún identifier de capture de la cláusula de captura puede ir precedido por & identifier. Del mismo modo, si la cláusula de captura incluye capture-default =, ningún capture de la cláusula de captura puede ir precedido por = identifier. Un identificador o this no puede aparecer más de una vez en una cláusula capture. 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
    [i, i]{};    // ERROR: i repeated
}

Un capture seguido de puntos suspensivos es una expansión del paquete, como se muestra en el siguiente ejemplo de plantilla variádica:

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

Puede utilizar expresiones lambda en el cuerpo de un método de clase. Pase el puntero this a la cláusula capture para proporcionar acceso a los métodos y miembros de datos de la clase contenedora. Para ver un ejemplo de cómo usar expresiones lambda con métodos de clase, consulte “Ejemplo: Usar una expresión lambda en un método” en Ejemplos de expresiones lambda.

Al usar la cláusula de captura, se recomienda tener en cuenta estos aspectos importantes, especialmente cuando se usan expresiones lambda con multithreading:

  • Se pueden usar capturas por referencia para modificar variables externas, pero no se pueden usar capturas por valor. (mutable permite modificar las copias, pero no los 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.

Lista de parámetros

La lista de parámetros (declarador lambda en la sintaxis estándar) es opcional y es similar a la lista de parámetros de una función.

Una expresión lambda puede tomar otra expresión lambda como argumento. Para obtener más información, consulte “Expresiones lambda de orden superior” en el tema Ejemplos de expresiones lambda.

La lista de parámetros es opcional, por lo que puede omitir los paréntesis vacíos si no pasa argumentos a la expresión lambda y su lambda-declarator: no contiene exception-specification, trailing-return-type o mutable.

Especificación mutable

Normalmente, el operador de llamada a función de una expresión lambda es const-by-value, pero mutable lo cancela. No genera miembros de datos mutable. 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 la palabra clave mutable.

Especificación de la excepción

Puede utilizar la especificación de excepción throw() para indicar que la expresión lambda no produce ninguna excepción. Como con las funciones normales, el compilador de Visual C++ genera la advertencia C4297 si una expresión lambda declara la especificación de excepción throw() y el cuerpo de la expresión lambda produce una excepción, como se muestra aquí:

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

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

Tipo devuelto

El tipo de valor devuelto de una expresión lambda se deduce automáticamente. No es necesario expresar la palabra clave auto, a menos que se especifique un trailing-return-type. El trailing-return-type es similar a la parte del tipo de valor devuelto de un método o función normal. 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 return-type de una expresión lambda si el cuerpo de la expresión lambda contiene una sola instrucción return o si la expresión lambda 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 is not valid

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

Cuerpo lambda

La parte del cuerpo lambda (compound-statement en la sintaxis estándar) de una expresión lambda puede contener todos los elementos que puede tener el cuerpo de un método o función normal. El cuerpo de una función normal y de una expresión lambda puede tener acceso a estos tipos de variables:

  • 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, las variables globales)

Además, una expresión lambda puede tener acceso a las variables que captura desde el ámbito que las contiene. Una variable se captura explícitamente si aparece en la cláusula capture de la expresión lambda. En caso contrario, la variable se captura implícitamente. El cuerpo de la expresión lambda usa el modo de captura predeterminado para tener acceso a las variables que se capturan implícitamente.

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

Resultado:

  

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.

Aunque una expresión lambda solo puede capturar variables que tengan duración de almacenamiento automática, puede utilizar variables que tengan 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 is not 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 generate_n de STL. 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 is not 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);
}

Resultado:

  

Para obtener más información, vea generate_n.

Modificadores específicos de Microsoft

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

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.

Vea también

Referencia

Expresiones lambda en C++

Ejemplos de expresiones lambda

generar

generate_n

for_each

Especificaciones de excepción

Advertencia del compilador (nivel 1) C4297

Modificadores específicos de Microsoft