c++ c++11 lambda language-lawyer

Capturar una referencia por referencia en una lambda C++ 11



c++11 language-lawyer (2)

TL; DR: El código en la pregunta no está garantizado por el estándar, y hay implementaciones razonables de lambdas que hacen que se rompa. Suponer que no es portátil y en su lugar utilizar

std::function<void()> make_function(int& x) { const auto px = &x; return [/* = */ px]{ std::cout << *px << std::endl; }; }

A partir de C ++ 14, puede eliminar el uso explícito de un puntero utilizando una captura inicializada, lo que obliga a crear una nueva variable de referencia para la lambda, en lugar de reutilizar la que está en el alcance adjunto:

std::function<void()> make_function(int& x) { return [&x = x]{ std::cout << x << std::endl; }; }

A primera vista, parece que debería ser seguro, pero la redacción del Estándar causa un pequeño problema:

Una expresión lambda cuyo alcance envolvente más pequeño es un alcance de bloque (3.3.3) es una expresión lambda local; cualquier otra expresión lambda no tendrá una captura-default o simple-capture en su lambda-introductor. El ámbito de alcance de una expresión lambda local es el conjunto de ámbitos de inclusión que incluye la función de delimitación más interna y sus parámetros.

...

Todas esas entidades capturadas implícitamente se declararán dentro del alcance de alcance de la expresión lambda.

...

[Nota: si una entidad es capturada implícita o explícitamente por referencia, invocar al operador de llamada a función de la expresión lambda correspondiente una vez que la vida de la entidad ha terminado es probable que dé como resultado un comportamiento indefinido. - nota final]

Lo que esperamos que ocurra es que x , como se usa dentro de make_function , se refiere a i en main() (ya que eso es lo que hacen las referencias), y la entidad i se captura por referencia. Como esa entidad aún vive en el momento de la llamada lambda, todo está bien.

¡Pero! "entidades capturadas implícitamente" debe estar "dentro del alcance de alcance de la expresión lambda", e i en main() no está en el alcance de alcance. :( A menos que el parámetro x cuente como "declarado dentro del alcance de alcance" aunque la propia entidad i se encuentre fuera del alcance de alcance.

Lo que esto parece es que, a diferencia de cualquier otro lugar en C ++, se crea una referencia a la referencia y la duración de una referencia tiene un significado.

Definitivamente, algo que me gustaría que el Standard aclare.

Mientras tanto, la variante que se muestra en la sección TL; DR es definitivamente segura porque el puntero se captura por valor (almacenado dentro del propio objeto lambda), y es un puntero válido para un objeto que dura a través de la llamada de la lambda. También esperaría que la captura por referencia en realidad termine almacenando un puntero de todos modos, por lo que no debería haber penalización en el tiempo de ejecución por hacer esto.

En una inspección más cercana, también imaginamos que podría romperse. Recuerde que en x86, en el código de máquina final, se accede a las variables locales y a los parámetros de función utilizando el direccionamiento relativo de EBP. Los parámetros tienen una compensación positiva, mientras que los locales son negativos. (Otras arquitecturas tienen diferentes nombres de registro, pero muchas funcionan de la misma manera.) De todos modos, esto significa que la captura por referencia se puede implementar capturando solo el valor de EBP. Entonces locales y parámetros pueden encontrarse de nuevo a través del direccionamiento relativo. Y, de hecho, creo que he oído hablar de implementaciones lambda (en lenguajes que tenían lambdas mucho antes que C ++) haciendo exactamente esto: capturar el "marco de pila" donde se definió el lambda.

Lo que esto implica es que cuando make_function regresa y su marco de pila desaparece, también lo hace toda la capacidad de acceder a los parámetros locales Y, incluso aquellos que son referencias.

Y el Estándar contiene la siguiente regla, probablemente específicamente para permitir este enfoque:

No se especifica si se declaran miembros de datos no estáticos adicionales no nombrados en el tipo de cierre para entidades capturadas por referencia.

Conclusión: El código en la pregunta no está garantizado por el estándar, y hay implementaciones razonables de lambdas que hacen que se rompa. Supongamos que no es portátil.

Considera esto:

#include <functional> #include <iostream> std::function<void()> make_function(int& x) { return [&]{ std::cout << x << std::endl; }; } int main() { int i = 3; auto f = make_function(i); i = 5; f(); }

¿Se garantiza que este programa generará 5 sin invocar un comportamiento indefinido?

Entiendo cómo funciona si capturo x por valor ( [=] ), pero no estoy seguro si estoy invocando un comportamiento indefinido al capturarlo por referencia. ¿Podría ser que termine con una referencia colgante después de que make_function regrese, o se garantiza que la referencia capturada funcionará siempre y cuando el objeto al que se hace referencia originalmente todavía esté allí?

Buscando respuestas definitivas basadas en estándares aquí :) Funciona bastante bien en la práctica hasta el momento ;)


El código está garantizado para funcionar.

Antes de profundizar en la redacción de las normas: es la intención del comité de C ++ que este código funcione. Sin embargo, la redacción, tal como está, se consideró insuficientemente clara al respecto (y, de hecho, las correcciones de errores realizadas después del estándar C ++ 14 rompieron el delicado arreglo que la hizo funcionar), por lo que el CWG número 2011 se planteó para aclarar las cosas. y está haciendo su camino a través del comité ahora. Hasta donde yo sé, ninguna implementación se equivoca.

Me gustaría aclarar un par de cosas, porque la respuesta de Ben Voigt contiene algunos errores que están creando cierta confusión:

  1. "Alcance" es una noción léxica estática en C ++ que describe una región del código fuente del programa en el que la búsqueda de nombre no calificada asocia un nombre particular con una declaración. No tiene nada que ver con la vida. Ver [basic.scope.declarative]/1 .
  2. Las reglas de "alcance de alcance" para lambdas son, asimismo, una propiedad sintáctica que determina cuándo se permite la captura. Por ejemplo:

    void f(int n) { struct A { void g() { // reaching scope of lambda starts here [&] { int k = n; }; // ...

    n está en el alcance aquí, pero el alcance de alcance de la lambda no lo incluye, por lo que no se puede capturar. Dicho de otra manera, el alcance de alcance de la lambda es qué tan lejos "arriba" puede alcanzar y capturar variables: puede llegar hasta la función de encerramiento (no lambda) y sus parámetros, pero no puede alcanzar fuera de eso y capturar declaraciones que aparecen afuera.

Entonces la noción de "alcanzar alcance" es irrelevante para esta pregunta. La entidad que se captura es el parámetro x make_function , que se encuentra dentro del alcance de alcance de la lambda.

OK, entonces veamos la redacción de la norma sobre este tema. Por [expr.prim.lambda] / 17, solo las expresiones id que se refieren a entidades capturadas por copia se transforman en un acceso de miembro en el tipo de cierre lambda; Las expresiones-id s que se refieren a entidades capturadas por referencia se dejan en blanco, y todavía denotan la misma entidad que habrían denotado en el alcance adjunto.

Esto parece inmediatamente malo: la vida útil de la referencia x ha terminado, entonces, ¿cómo podemos referirnos a ella? Bueno, resulta que casi no hay forma de referirse a una referencia fuera de su tiempo de vigencia (puede ver una declaración de ella, en cuyo caso está dentro del alcance y, por lo tanto, presumiblemente OK, o es una clase miembro, en cuyo caso la clase en sí misma debe estar dentro de su ciclo de vida para que la expresión de acceso miembro sea válida). Como resultado, el estándar no tenía ninguna prohibición de usar una referencia fuera de su tiempo de vida hasta hace muy poco.

La redacción lambda aprovechó el hecho de que no hay penalización por usar una referencia fuera de su tiempo de vida, y por lo tanto no necesita dar ninguna regla explícita para el acceso a una entidad capturada por medios de referencia, solo significa que usted usa eso entidad; si es una referencia, el nombre denota su inicializador. Y así es como se garantizó que esto funcionará hasta hace muy poco tiempo (incluso en C ++ 11 y C ++ 14).

Sin embargo, no es del todo cierto que no pueda mencionar una referencia fuera de su tiempo de vida; en particular, puede referenciarlo desde su propio inicializador, desde el inicializador de un miembro de clase anterior a la referencia, o si se trata de una variable de ámbito de espacio de nombres y accede desde otro global que se inicializa antes que la referencia. El CWG issue 2012 se introdujo para corregir esa supervisión, pero inadvertidamente rompió la especificación para la captura de lambda por referencia de referencias. Deberíamos corregir esta regresión antes de que nazca C ++ 17; He presentado un comentario del Cuerpo Nacional para asegurarme de que se prioriza adecuadamente.