functions functional funciones expressions examples c++ c++11 lambda parameter-passing std-function

functional - ¿Cómo se representan y pasan C++ 11 lambdas?



lambda functions (3)

Pasar un lambda es realmente fácil en c ++ 11:

func( []( int arg ) { // code } ) ;

Pero me pregunto, ¿cuál es el costo de pasar una lambda a una función como esta? ¿Qué pasa si func pasa el lambda a otras funciones?

void func( function< void (int arg) > f ) { doSomethingElse( f ) ; }

¿Es caro el paso de la lambda? Como a un objeto de function se le puede asignar 0,

function< void (int arg) > f = 0 ; // 0 means "not init"

me lleva a pensar que los objetos de función tipo de actuar como punteros. Pero sin el uso de new , significa que podrían ser como una struct o clases con tipado de valor, que de forma predeterminada es la asignación de la pila y la copia para los miembros.

¿Cómo se pasa un "cuerpo de código" de C ++ 11 y un grupo de variables capturadas cuando pasa un objeto de función "por valor"? ¿Hay una gran cantidad de copia en exceso del cuerpo del código? ¿Debo marcar cada objeto de function pasado con const& para que no se haga una copia?

void func( const function< void (int arg) >& f ) { }

¿O los objetos funcionales de alguna manera pasan de forma diferente que las estructuras normales de C ++?


Si la lambda se puede hacer como una función simple (es decir, no captura nada), entonces se hace exactamente de la misma manera. Especialmente como estándar requiere que sea compatible con el puntero-a-función de estilo antiguo con la misma firma. [EDITAR: no es preciso, ver discusión en comentarios]

Por lo demás, depende de la implementación, pero no me preocuparé más adelante. La implementación más directa no hace más que llevar la información. Exactamente tanto como lo solicitó en la captura. Entonces el efecto sería el mismo que si lo hicieras creando una clase manualmente. O use alguna variante de std :: bind.


Ver también C ++ 11 implementación lambda y modelo de memoria

Una expresión lambda es solo eso: una expresión. Una vez compilado, da como resultado un objeto de cierre en tiempo de ejecución.

5.1.2 Expresiones lambda [expr.prim.lambda]

La evaluación de una expresión lambda da como resultado un prvalue temporal (12.2). Este temporal se llama el objeto de cierre.

El objeto en sí está definido por la implementación y puede variar de compilador a compilador.

Aquí está la implementación original de lambdas en clang https://github.com/faisalv/clang-glambda


Descargo de responsabilidad: mi respuesta es algo simplificada en comparación con la realidad (pongo algunos detalles a un lado) pero el panorama está aquí. Además, el estándar no especifica completamente cómo lambdas o std::function deben implementarse internamente (la implementación tiene cierta libertad) así que, al igual que cualquier discusión sobre detalles de implementación, su compilador puede o no hacerlo exactamente de esta manera.

Pero, de nuevo, este es un tema bastante similar a VTables: el estándar no exige mucho, pero cualquier compilador sensato es aún bastante probable que lo haga de esta manera, por lo que creo que vale la pena investigarlo un poco. :)

Lambdas

La forma más directa de implementar un lambda es una struct anónima:

auto lambda = [](Args...) -> Return { /*...*/ }; // roughly equivalent to: struct { Return operator ()(Args...) { /*...*/ } } lambda; // instance of the anonymous struct

Al igual que cualquier otra clase, cuando pasa sus instancias, nunca debe copiar el código, solo los datos reales (aquí, ninguno en absoluto).

Los objetos capturados por valor se copian en la struct :

Value v; auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ }; // roughly equivalent to: struct Temporary { // note: we can''t make it an anonymous struct any more since we need // a constructor, but that''s just a syntax quirk const Value v; // note: capture by value is const by default unless the lambda is mutable Temporary(Value v_) : v(v_) {} Return operator ()(Args...) { /*... use v, captured by value...*/ } } lambda(v); // instance of the struct

De nuevo, pasarlo solo significa que pasa los datos ( v ) y no el código en sí.

Del mismo modo, los objetos capturados por referencia se referencian en la struct :

Value v; auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ }; // roughly equivalent to: struct Temporary { Value& v; // note: capture by reference is non-const Temporary(Value& v_) : v(v_) {} Return operator ()(Args...) { /*... use v, captured by reference...*/ } } lambda(v); // instance of the struct

Eso es prácticamente todo cuando se trata de lambdas (excepto los pocos detalles de implementación que omití, pero que no son relevantes para entender cómo funciona).

std::function

std::function es un contenedor genérico alrededor de cualquier tipo de functor (lambdas, funciones independientes / estáticas / miembros, clases de funtores como las que mostré, ...).

Las std::function internas de std::function son bastante complicadas porque deben soportar todos esos casos. Dependiendo del tipo exacto de functor, esto requiere al menos los siguientes datos (detalles de implementación de dar o recibir):

  • Un puntero a una función autónoma / estática.

O,

  • Un puntero a una copia [ver la nota a continuación] del funtor (dinámicamente asignado para permitir cualquier tipo de funtor, como lo mencionó correctamente).
  • Un puntero a la función miembro a llamar.
  • Un puntero a un asignador que es capaz de copiar tanto el funtor como a sí mismo (dado que se puede usar cualquier tipo de funtor, el puntero a funtor debe ser void* y, por lo tanto, tiene que haber tal mecanismo, probablemente usando polimorfismo, también conocido como .base class + virtual methods, la clase derivada se genera localmente en la template<class Functor> function(Functor) constructores de template<class Functor> function(Functor) ).

Dado que no sabe de antemano qué tipo de functor tendrá que almacenar (y esto se hace evidente por el hecho de que std::function puede reasignarse), entonces tiene que hacer frente a todos los casos posibles y tomar la decisión en tiempo de ejecución.

Nota: No sé dónde lo exige el Estándar, pero definitivamente es una copia nueva, el functor subyacente no se comparte:

int v = 0; std::function<void()> f = [=]() mutable { std::cout << v++ << std::endl; }; std::function<void()> g = f; f(); // 0 f(); // 1 g(); // 0 g(); // 1

Por lo tanto, cuando pasa una std::function al respecto implica al menos esos cuatro punteros (y de hecho en GCC 4.7 64 bits sizeof(std::function<void()> 32, que son cuatro punteros de 64 bits) y opcionalmente dinámicamente copia asignada del funtor (que, como ya dije, solo contiene los objetos capturados, no se copia el código ).

Respuesta a la pregunta

¿Cuál es el costo de pasar una lambda a una función como esta? [contexto de la pregunta: por valor ]

Bueno, como puedes ver, depende principalmente de tu functor (ya sea un functor de struct hecho a mano o un lambda) y las variables que contiene. La sobrecarga en comparación con pasar directamente un funtor de struct por valor es bastante despreciable, pero por supuesto es mucho más alta que pasar un funtor de struct por referencia.

¿Debo marcar cada objeto de función pasado con const& para que no se haga una copia?

Me temo que esto es muy difícil de responder de una manera genérica. Algunas veces querrá pasar de referencia const , a veces por valor, a veces por referencia de valor real para poder moverlo. Realmente depende de la semántica de tu código.

Las reglas sobre cuál elegir debería ser un tema totalmente diferente de la OMI, solo recuerde que son las mismas que para cualquier otro objeto.

De todos modos, ahora tiene todas las claves para tomar una decisión informada (nuevamente, dependiendo de su código y su semántica ).