De acuerdo con las fuentes que he encontrado, una expresión lambda es esencialmente implementada por el compilador creando una clase con el operador de llamada de función sobrecargado y las variables referenciadas como miembros. Esto sugiere que el tamaño de las expresiones lambda varía, y dadas suficientes referencias variables, ese tamaño puede ser arbitrariamente grande .
An std::function
debe tener un tamaño fijo , pero debe poder envolver cualquier tipo de invocables, incluidas las lambdas del mismo tipo. ¿Cómo se implementa? Si std::function
utiliza internamente un puntero a su destino, ¿qué sucede cuando la std::function
instancia se copia o se mueve? ¿Hay alguna asignación de montón involucrada?
std::function
un tiempo. Es esencialmente una clase de identificador para un objeto polimórfico. Se crea una clase derivada de la clase base interna para contener los parámetros, asignados en el montón; luego, el puntero a esto se mantiene como un subobjeto destd::function
. Creo que utiliza el recuento de referencias comostd::shared_ptr
para manejar la copia y el movimiento.Respuestas:
La implementación de
std::function
puede diferir de una implementación a otra, pero la idea central es que usa borrado de tipo. Si bien hay varias formas de hacerlo, puede imaginar que una solución trivial (no óptima) podría ser así (simplificada para el caso específico destd::function<int (double)>
por simplicidad):En este enfoque simple, el
function
objeto almacenaría solounique_ptr
a en un tipo base. Para cada funtor diferente utilizado con elfunction
, se crea un nuevo tipo derivado de la base y se crea una instancia de un objeto de ese tipo de forma dinámica. Elstd::function
objeto es siempre del mismo tamaño y asignará el espacio necesario para los diferentes functores del montón.En la vida real existen diferentes optimizaciones que proporcionan ventajas de rendimiento pero complicarían la respuesta. El tipo podría usar optimizaciones de objetos pequeños, el despacho dinámico se puede reemplazar por un puntero de función libre que toma el functor como argumento para evitar un nivel de indirección ... pero la idea es básicamente la misma.
Con respecto a la cuestión de cómo se
std::function
comportan las copias del , una prueba rápida indica que se realizan copias del objeto invocable interno, en lugar de compartir el estado.La prueba indica que
f2
obtiene una copia de la entidad invocable, en lugar de una referencia. Si la entidad invocable fuera compartida por los diferentesstd::function<>
objetos, la salida del programa habría sido 5, 6, 7.fuente
std::function
sería correcta si se copiara el objeto interno, y no creo que ese sea el caso. (piense en una lambda que captura un valor y es mutable, almacenada dentro de unstd::function
, si el estado de la función se copió, la cantidad de copias destd::function
dentro de un algoritmo estándar podría dar como resultado resultados diferentes, lo cual no es deseado.std::function
activará una asignación.La respuesta de @David Rodríguez: dribeas es buena para demostrar el borrado de tipo, pero no lo suficientemente bueno, ya que el borrado de tipo también incluye cómo se copian los tipos (en esa respuesta, el objeto de función no se podrá copiar). Estos comportamientos también se almacenan en el
function
objeto, además de los datos de functor.El truco, utilizado en la implementación de STL de Ubuntu 14.04 gcc 4.8, es escribir una función genérica, especializarla con cada tipo de functor posible y convertirlos en un tipo de puntero de función universal. Por lo tanto, se borra la información de tipo .
He improvisado una versión simplificada de eso. Espero que ayude
También hay algunas optimizaciones en la versión STL.
construct_f
ydestroy_f
se mezclan en un puntero de función (con un parámetro adicional que le dice qué hacer) como para guardar algunos bytesunion
, de modo que cuando unfunction
objeto se construye a partir de un puntero de función, se almacenará directamente en elunion
espacio en lugar de en el montónQuizás la implementación de STL no sea la mejor solución, ya que he oído hablar de una implementación más rápida . Sin embargo, creo que el mecanismo subyacente es el mismo.
fuente
Para ciertos tipos de argumentos ("si el objetivo de f es un objeto invocable pasado a través de
reference_wrapper
o un puntero de función"),std::function
el constructor no permite ninguna excepción, por lo que el uso de memoria dinámica está fuera de discusión. Para este caso, todos los datos deben almacenarse directamente dentro delstd::function
objeto.En el caso general (incluido el caso lambda),
std::function
se permite el uso de memoria dinámica (a través del asignador estándar o un asignador pasado al constructor) según lo considere la implementación. El estándar recomienda que las implementaciones no usen memoria dinámica si se puede evitar, pero como bien dice, si el objeto de función (no elstd::function
objeto, sino el objeto que se envuelve dentro de él) es lo suficientemente grande, no hay forma de evitarlo, ya questd::function
tiene un tamaño fijo.Este permiso para lanzar excepciones se otorga tanto al constructor normal como al constructor de copia, que también permite de manera bastante explícita asignaciones de memoria dinámica durante la copia. Para los movimientos, no hay ninguna razón por la que sea necesaria la memoria dinámica. El estándar no parece prohibirlo explícitamente, y probablemente no pueda hacerlo si el movimiento podría llamar al constructor de movimiento del tipo del objeto envuelto, pero debería poder asumir que si tanto la implementación como sus objetos son sensibles, el movimiento no causará cualquier asignación.
fuente
An lo
std::function
sobrecargaoperator()
convirtiéndolo en un objeto functor, lambda funciona de la misma manera. Básicamente, crea una estructura con variables miembro a las que se puede acceder dentro de laoperator()
función. Entonces, el concepto básico a tener en cuenta es que una lambda es un objeto (llamado functor u objeto de función), no una función. El estándar dice que no se use memoria dinámica si se puede evitar.fuente
std::function
? Esa es la pregunta clave aquí.std::function
objetos tienen el mismo tamaño y no son del tamaño de las lambdas contenidas.std::vector<T...>
objeto tiene un tamaño fijo (copiletime) independiente de la instancia del asignador real / número de elementos.std::function<void ()> f;
no es necesario asignar allí, lostd::function<void ()> f = [&]() { /* captures tons of variables */ };
más probable es que asigne.std::function<void()> f = &free_function;
probablemente tampoco asigna ...