c++ - resueltos - lambda sintaxis
En la sintaxis de las funciones lambda, ¿para qué sirve una ''lista de captura''? (5)
Tomado de una respuesta a esta pregunta , como ejemplo, este es un código que calcula la suma de elementos en un std::vector
:
std::for_each(
vector.begin(),
vector.end(),
[&](int n) {
sum_of_elems += n;
}
);
Entiendo que las funciones lambda son simplemente funciones sin nombre.
Entiendo la sintaxis de las funciones lambda como se explica aquí .
No entiendo por qué las funciones lambda necesitan la lista de captura, mientras que las funciones normales no.
- ¿Qué información adicional proporciona una lista de captura?
- ¿Por qué las funciones normales no necesitan esa información?
- ¿Son las funciones lambda más que simples funciones sin nombre?
¿Son las funciones lambda más que simples funciones sin nombre?
¡SÍ! Además de no tener nombre, pueden referirse a las variables en el ámbito incluido léxicamente. En su caso, un ejemplo sería la variable sum_of_elems
(no es un parámetro ni una variable global, supongo). Las funciones normales en C ++ no pueden hacer eso.
¿Qué información adicional proporciona una lista de captura?
La lista de captura proporciona la
- lista de variables a capturar con
- información sobre cómo deben ser capturados (por referencia / por valor)
En otros idiomas (por ejemplo, funcionales), esto no es necesario, porque siempre se refieren a los valores de una manera (por ejemplo, si los valores son inmutables, la captura sería por valor; otra posibilidad es que todo sea una variable en el montón). así que todo es capturado por la referencia y no tiene que preocuparse por su vida útil, etc.). En C ++, tiene que especificarlo para elegir entre referencia (puede cambiar la variable exterior, pero explotará cuando la lambda sobreviva a la variable) y valor (todos los cambios aislados dentro de la lambda, pero vivirán tanto como la lambda, básicamente , será un campo en una estructura que representa la lambda).
Puede hacer que el compilador capture todas las variables necesarias utilizando el símbolo predeterminado de captura , que solo especifica el modo de captura predeterminado (en su caso: &
=> reference; =
sería valor). En ese caso, básicamente se capturan todas las variables a las que se hace referencia en la lambda desde el ámbito externo.
A partir del enlace de sintaxis que proporcionó, la lista de captura "define qué elementos externos deben estar disponibles dentro del cuerpo de la función y cómo"
Las funciones ordinarias pueden usar datos externos de varias maneras:
- Campos estáticos
- Campos de instancia
- Parámetros (incluyendo parámetros de referencia)
- Globales
Lambda agrega la capacidad de tener una función sin nombre dentro de otra. La lambda puede utilizar los valores que especifique. A diferencia de las funciones ordinarias, esto puede incluir variables locales desde una función externa.
Como dice esa respuesta, también puede especificar cómo desea capturar. awoodland da algunos ejemplos en otra respuesta . Por ejemplo, puede capturar una variable externa por referencia (como un parámetro de referencia), y todas las demás por valor:
[=, &epsilon]
EDITAR:
Es importante distinguir entre la firma y lo que la lambda usa internamente. La firma de un lambda es la lista ordenada de tipos de parámetros, más el tipo del valor devuelto.
Por ejemplo, una función unaria toma un solo valor de un tipo particular y devuelve un valor de otro tipo.
Sin embargo, internamente puede utilizar otros valores. Como ejemplo trivial:
[x, y](int z) -> int
{
return x + y - z;
}
El que llama al lambda solo sabe que toma un int
y devuelve un int
. Sin embargo, internamente sucede que usa otras dos variables por valor.
Considera esto:
std::function<int()>
make_generator(int const& i)
{
return [i] { return i; };
}
// could be used like so:
int i = 42;
auto g0 = make_generator(i);
i = 121;
auto g1 = make_generator(i);
i = 0;
assert( g0() == 42 );
assert( g1() == 121 );
Tenga en cuenta que en esta situación, los dos generadores que se han creado tienen cada uno su propio i
. Esto no es algo que se pueda recrear con funciones ordinarias, y por eso los usuarios no usan listas de captura. Las listas de captura resuelven una versión del problema funarg .
¿Son las funciones lambda más que simples funciones sin nombre?
Esa es una pregunta muy inteligente. Las expresiones lambda que se crean son de hecho más poderosas que las funciones regulares: son closures (y el Estándar se refiere a los objetos que las expresiones lambda crean como ''objetos de cierre''). Para decirlo brevemente, un cierre es una función combinada con un alcance limitado. La sintaxis de C ++ ha elegido representar el bit de función en una forma familiar (lista de argumentos con tipo de retorno retrasado con cuerpo de función, algunas partes opcionales), mientras que la lista de captura es la sintaxis que especifica qué variable local participará en el ámbito delimitado (no local). las variables se introducen automáticamente).
Tenga en cuenta que otros idiomas con cierres generalmente no tienen una construcción similar a las listas de captura de C ++. C ++ ha hecho la elección de diseño de listas de captura debido a su modelo de memoria (la variable local solo vive el alcance local) y su filosofía de no pagar por lo que no se usa (haciendo que las variables locales se conviertan en automágicamente más largas si La captura puede no ser deseable en todos y cada uno de los casos).
El problema básico que estamos tratando de resolver es que algunos algoritmos esperan una función que solo toma un conjunto particular de argumentos (un int
en su ejemplo). Sin embargo, queremos que la función sea capaz de manipular o inspeccionar algún otro objeto, tal vez así:
void what_we_want(int n, std::set<int> const & conditions, int & total)
{
if (conditions.find(n) != conditions.end()) { total += n; }
}
Sin embargo, todo lo que podemos dar a nuestro algoritmo es una función como void f(int)
. Entonces, ¿dónde ponemos los otros datos?
Puede mantener los otros datos en una variable global, o puede seguir el enfoque tradicional de C ++ y escribir un functor:
struct what_we_must_write
{
what_we_must_write(std::set<int> const & s, int & t)
: conditions(s), total(t)
{ }
void operator()(int n)
{
if (conditions.find(n) != conditions.end()) { total += n; }
}
private:
std::set<int> const & conditions;
int & total;
};
Ahora podemos llamar al algoritmo con un functor adecuadamente inicializado:
std::set<int> conditions;
int total;
for_each(v.begin(), v.end(), what_we_must_write(conditions, total));
Finalmente, un objeto de cierre (que se describe con una expresión lambda ) es solo eso: una forma breve de escribir un functor. El equivalente del funtor anterior es la lambda.
auto what_we_get = [&conditions, &total](int n) -> void {
if (condiditons.find(n) != conditions.end()) { total += n; } };
Las listas de captura de mano corta [=]
y [&]
simplemente capturan "todo" (respectivamente por valor o por referencia), lo que significa que el compilador resuelve la lista de captura concreta para usted (en realidad no pone todo en la lista de captura). Objeto de cierre, pero solo las cosas que necesitas).
Entonces, en pocas palabras: un objeto de cierre sin captura es como una función libre, y un cierre con captura es como un objeto funtor con objetos miembros privados adecuadamente definidos e inicializados.
Quizás sea mejor pensar en una expresión lambda como un objeto que tiene el operador ()
, en lugar de una simple función. El "objeto" de lambda puede tener campos que recuerdan (o "capturan") las variables fuera de lambda en el momento de la construcción de lambda, que se utilizarán más adelante en el momento de la ejecución de lambda.
La lista de captura es simplemente una declaración de tales campos.
(Ni siquiera necesita especificar la lista de captura usted mismo: la sintaxis [&]
o [=]
indica al compilador que determine la lista de captura automáticamente, en función de las variables del ámbito externo que se usan en el cuerpo lambda).
Una función normal no puede contener el estado, no puede "recordar" los argumentos de una vez para usarlos en otra. Una lambda puede. Una clase elaborada manualmente con un operador implementado por el usuario ()
(también conocido como "functor") también puede, pero es mucho menos conveniente sintácticamente.