Los objetos invocables (callables) se refiere aquellos elementos tipo objeto
que pueden ser llamados desde el código usando algún tipo de sintaxis de llamada
a función. Se distinguen de las funciones regulares en que son de tipo objeto
(instancias), no son de tipo función. Una función regular no es un objeto, ni
es de tipo objeto. Por ejemplo, no se puede usar el operador sizeof
sobre una
función regular1.
Todo objeto que pueda ser invocado usando la sintaxis de llamado a función es
considerado un objeto invocable. Por ejemplo: Punteros a funciones (miembro,
y no-miembro), Funtores (un tipo clase-objeto-función: instancia de una clase
que tiene sobrecargado el operador de ejecución operator ()
). En pocas
palabras, es un objeto que actúa como una función. Un uso importante de los
objetos invocables es poder ser llamados en algoritmos.
Invocables en C++ moderno
Previo a C++11 los objetos invocables tenían ciertas limitaciones, era bastante complejo declarar uno en ámbitos locales, y la sintaxis podría llegar a ser confusa, especialmente para punteros a funciones.
Con la llegada de las expresiones lambda aumentaron las facilidades al hacer
uso de este tipo de objetos. También se agregó std::function
que es un
envoltorio de objetos invocables, de propósito general.
Intuitivamente se puede pensar que el uso de std::function
es el ideal, sin
embargo, éste tiene algunas desventajas con respecto a las expresiones lambda,
como que no maneja tipos que sean solo movibles. Adicionalmente, es un objeto
pesado porque necesita manejar todos los objetos invocables, y para hacer eso
requiere mecánicas internas avanzadas como manipulación de tipos o incluso
asignación de memoria dinámica2.
Lambdas sin estado
Un objeto de una clase con una variable miembro puede almacenar un estado guardando información en esa variable. Si una clase no tiene variable-miembro, por ejemplo solo tiene funciones, entonces no almacena estados, es una clase sin estado.
Esto funciona de manera similar para las expresiones lambda; una lambda sin estado es aquella que no captura ninguna variable. Tal y como pasa con un funtor, una estructura o clase sin variables miembro, solamente con el operador de ejecución, se toma como una clase vacía y su tamaño es 1 Byte.
Comparativa de tamaños de objetos invocables
Teniendo la siguiente función regular que muestra en consola el mensaje “Hola mundo”:
void imprime() {
fmt::print("Hola mundo\n");
}
Se pueden crear los siguientes objetos invocables:
void (*punteroAImprime)() = imprime; // puntero a función
struct { void operator()() const { imprime(); } } funtor; // funtor
const auto lambdaImprime = [](){ imprime(); }; // lambda
const std::function<void(void)> functionImprime = imprime; // function
fmt::print("sizeof(lambdaImprime) es: {:#2} Bytes \n", sizeof(lambdaImprime));
fmt::print("sizeof(functionImprime) es: {:#2} Bytes \n", sizeof(functionImprime));
La expresión lambda del ejemplo anterior es una lambda sin estado.
En GCC y clang el tamaño de lambdaImprime
es de 1 Byte, mientras
que el tamaño de functionImprime
es 32 Bytes.
Fuentes
- Lambdas en C++ - Bartlomiej Filipek
- Stateless lambda
- Terminology and Conventions: Effective Modern C++ - Scott Meyers.
- std::function
-
Ver Operador sizeof ↩
-
El tipo de una expresión lambda: Bartlomiej Filipek - Lambdas en C++ ↩
Deja un comentario