You are currently browsing the tag archive for the ‘función’ tag.

Punteros a funciones son variables que guardan una dirección de memoria: la dirección de una función. Punteros son una especie de referencia. En muchos lenguajes de programación existe el concepto de referencias a funciones que se pueden guardar en una variable. En este artículo hablamos de punteros de funciones como se usan en los lenguajes C y C++. Sin embargo, los conceptos se dejan aplicar a cualquier tipo de referencias a funciones – también a aquella otra que usa C++: los functores.

El caso clásico de usar punteros a funciones es una cola de eventos. Si programas una cola de eventos, entonces no sabes cómo se llamarán las funciones (manejadores de eventos) que se registrarán. Por eso no puedes llamarles por nombre. De hecho, colas de eventos se implementan frecuentemente en una biblioteca cuyo desarrollo es independiente de la programación de las funciones a registrar. Pues, no sabrás los nombres de las funciones y tampoco necesitas saberlos.

Tu cola de eventos suministras una función de registro. Esta función tiene un parámetro cuyo tipo es una función. Es decir, aunque no sabes los nombres de las funciones, sí, obligas a que tengan un tipo conocido: el que defines tú.

Tu cola de eventos tiene una lista con punteros a funciones que tienen este tipo. Por este punto de vista, una cola de evento no es muy diferente a una base de datos, donde el usuario puede guardar (es decir, registrar) datos. La differencia es que a los datos de una cola de eventos se puede llamar como función.

La ventaja de todo esto: Puedes llamar a un número arbitrario de funciones sin necesitar la declaración de estas funciones de antemano.

No obstante, esto lleva también una pequeña desventaja. Herramientas de análisis de código pueden extraer del código, qué función llama a qué otra función. Esto es una inforamción útil cuando quieres saber a qué “clientes” afectas al cambiar una función. No obstante, este análisis no funciona con punteros a funciones, ya que el valor de ellos sólo se sabrá a tiempo de ejecución. Sin embargo, esto no suele ser una restricción importante. Las funciones que se llaman mediante puntero suelen ser pocas y bien claras. Por ejemplo, una función de nombre OnMouseClick es fácil de reconocer como manenjador de eventos. Además, no es buen estilo de llamar a manejadores de eventos por nombre desde otras funciones. Así no lo harías, ¿verdad?

Muchas colas de eventos suelen guardar más datos que el puntero a la función. La razón es que no quieres escribir 20 manejadores para eventos que sólo se diferencian por un parámetro. La función de registro te permite añadir un dato de tipo identificador, que pasa como parámetro al manejador de evento. El manejador de evento es, por lo tanto, un tipo de función que lleva este parámetro. Dentro del manejador puedes leer este dato y saber así, a base de qué registro fue llamado. Así puedes registrar la misma función para varios eventos.

La pregunta es: ¿qué es el tipo más adecuado para este dato identificador? Pues, muchas implementaciones usan un tipo “cualquier cosa”. En Java, esto sería un Object, en C y C++ gusta un puntero de tipo void* con un parámetro adicional con el tamaño del buffer a que el puntero apunta. De esta forma puedes registrar cualquier dato en la cola de eventos, sin que la cola necesita conocer su estructura. El tipo real sabrá el manejador de eventos, que convierte el tipo “cualquier cosa” en el tipo que realmente espera.

// Al registrar el manejador usas un identificador de tipo struct Id
struct Id
{
    int index;
    char subindex;
};
Id mi_id = { 5, 1 };
cola_de_eventos.registra(mi_callback, mi_id);

// Tu manejador de eventos haría esto
void mi_callback(const void*const datos)
{
    // Tu manejador sabe que está por detrás de estos "datos"
    const Id*const id = static_cast<const Id*const>(datos);
    
    // Aquí haría algo con estos datos como
    // switch (id.index) ...
}

Ten en cuenta que los datos mencionados arriba se guardan junto con la dirección a la función de antemano. Pues, identifican el registro. No es lo mismo que parámetros que se producen junto con el evento. Por ejemplo, un manejador de OnMouseClick esperaría las coordenadas del ratón, donde tuvo lugar el click. Cómo diferentes eventos pueden producir diferentes datos subyacentes, se encuentra a menudo la idea de usar un tipo “cualquier cosa” también para estos parámetros generados en el evento.

Cuando creas un registro de funciones como una cola de eventos, te deberías preguntar, qué tipo de datos quieres permitir. Un tipo “cualquier cosa” da mucha flexibilidad – también para su abuso. Y no es necessario si sólo quieres permitir un identificador.

Si prefieres usar clases en lugar de punteros, define quién debe hacer la limpieza en la memoria. Pasar un objeto por referencia obliga a guardarlo hasta que ya no se usa. Pasar un objeto por copia deja claro al autor del manejador que se debe hacer una copia antes de terminar el manejador si no quiere perder los datos.

Piensa también que C++ permite functores. Functores son clases que sobrecargan el operador (). Así se puede tratar una instancia de esta clase como función: mi_instancia() llama realmente a mi_instancia.operator(). Functores son instancias de clases y estas se protegen mejor contra un fraude como lo permitirían los punteros. ¡Imagínate que el autor de la cola de eventos es malo y no llama al puntero de función que registraste sino a esta dirección más cuatro bytes!

Como ves, punteros a funciones son muy útiles para determinadas tareas como cola de eventos. Puede tener sentido guardar datos de identificación junto con el puntero y puede ser útil manejar un tipo “cualquier cosa” para pasar datos de tipo arbitrario. No obstante es importante de pensar bien, si es mejor restringir la flexibilidad.

Lectura adicional

En este artículo quiero tratar como se puede usar la sentencia return para asegurar succesivamente la calidad de datos y obtener un código más limpio.

El problema

La programación estructurada se inventó para evitar el comando goto. La sentencia return también es una especie de goto ya que salta fuera de la subrutina aunque todavía queda código detrás a ejecutar. Por eso hay guías de estilo que piden que una función sólo puede tener una sentencia de return al final – normalmente para devolver el resultado de la función.

Esto es una buena regla en un cálculo lineal, es decir, cuando no se necesitan comprobar errores antes de seguir. Cálculos matemáticos son muchas veces de este tipo. La suma de a y b no falla aunque las variables contengan datos inválidos.

No obstante, en la mayoría de los casos necesitamos la comprobación de errores. En un escenario real el tratamiento de errores ocupa a menudo más espacio que el cálculo principal.

Veamos un ejemplo. Queremos leer algunos bytes de un fichero. Un programa simple tendría pocas líneas.

char buffer[1000];                       // Un buffer
FILE* f = fopen("mi_fichero.txt", "r");  // Abre fichero
fread(buffer, 1, sizeof(buffer), f);     // Lee algo
fclose(f);                               // Cierra fichero

Si tenemos en cuenta los posibles errores, la misma funcionalidad se extendería así:

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre fichero
FILE* f = fopen("mi_fichero.txt", "r");  

// Si podía abrir el fichero
if (f != NULL)  
{
	// Entonces lee del fichero
    const size_t read_result = 
		fread(buffer, 1, sizeof(buffer), f);
		
	// Si podía leer el fichero
    if (read_result == sizeof(buffer) 
	{
		// Haz algo
	}
	else
    {
		// Marca error si no podía leer del fichero
        error = true; 
    }
	
	// En todo caso cierra el fichero
    fclose(f);
}
else
{
	// Marca error si no podía abrir el fichero
    error = true;
}

Para cada comprobación que necesitamos para seguir adelante debemos añadir un if anidado. Esto ensucia tanto el código que podemos perder la vista sobre la esencia de la función. La idea principal de abrir, leer y cerrar un fichero queda obfuscada por tanta comprobación de error. Y este ejemplo es todavía bastante simple en comparación con un código real.

Usar múltiples return

Si permitimos el uso de varios return en medio, entonces podemos reducir el número de if anidados.

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre el fichero
FILE* f = fopen("mi_fichero.txt", "r");
if (f == NULL)
{
    return;
}

// Lee el fichero
const size_t read_result = 
	fread(buffer, 1, sizeof(buffer), f);
if (read_result != sizeof(buffer) 
{
    error = true;
}

// Cierra el fichero
fclose(f);

La filosofía de este procedimiento es consultar en cada paso, si los datos obtenidos son buenos y me permiten continuar. Si no puedo continuar, entonces termina la ejecución. Por ejemplo, si no encuentro el fichero en fopen, entonces no hay nada que leer y debo cancelar la función. Ya no tengo el problema de if anidados y puedo fácilmente añadir una condición más si hace falta.

Los chequeos con return en medio ayudan también a depurar el código. Si la ejecución del programa ha llegado a un cierto punto dentro de una función, entonces ya sé que todos los datos que se hayan obtenido anteriormente fueron buenos. La estructura del código me permite concentrar en cada paso del algoritmo sin dejar de hacer un chequeo exhaustivo de posibles errores.

Un return en medio de la función devuelve típicamente un código de error. Un return que devuevle un código de error notifica una excepción, es decir, un error no deseado. Por lo tanto suele ser posible lanzar una excepción en lugar de devolver un código de error. Cual de las dos formas es preferible es una decisión del diseño del programa a alto nivel y no a nivel de función. Hay lenguajes como C en que no se pueden lanzar excepciones. En otros lenguajes como Java el uso de excepciones es lo habitual.

Por cierto, para salir o saltar en bucles suele existir una sentencia equivalente al return. En los lenguaje C y Java se llaman break y continue, respectivamente. Son menos usado que el return y probablemente con razón. El enunciado return sale de la función sin más buscar. En cambio, las interrupciones de bucles saltan al inicio o al final del bucle que a veces cuesta encontrar – sobre todo en funciones largas y con bucles anidados.

Una solución curiosa

En este apartado presento una idea un poco peculiar. No recomiendo usarla y la presento sólo como una curiosidad.

La idea consiste en encadenar todas las comprobaciones en un sólo if y hacer de paso todo el trabajo.

char buffer[1000];
bool error = true;
FILE* f;
if (f = fopen("mi_fichero.txt", "r") &&
    sizeof(buffer) == fread(buffer, 1, sizeof(buffer), f))
{
    error = false;
}
if (f)
{
    fclose(f);
}

Esta construcción funciona así: se ejecuta fopen y se asigna el puntero que devuelve a f. El valor de una asignación es el valor asignado, es decir el puntero devuelto. Si este puntero vale NULL, entonces se interpreta como un false booleano. En este caso ya no se evalúa el segundo operador de la expresión && y el programa no entra en el if. En el caso que el puntero f no es nulo, se ejecuta fread y se compara con el valor esperado. Si no lo es, entonces no se entra en el if.

La condición del if sólo es cierta si todos los operadores de la expresión devuelven valores no nulos. Al primer valor nulo, se termina la evaluación de la expresión booleana y no se ejecutan las funciones siguientes. Esta forma compacta bastante el código sin dejar de comprobar errores.

Este ejemplo demuestra que alguien entiende mucho de las caracterísicas del lenguaje de programación pero quizá no tanto de la semántica del código. Muchos lenguajes como C y Java permiten asignaciones en expresiones booleanas aunque es mala práctica, ya que una expresión booleana, por semántica, es un valor y no un contenedor de datos. En cambio, este ejemplo no sólo asigna el puntero de fichero sino pone todo el código ejecutable en la condición del if.

En lenguaje humano, un uso semánticamente correcto del if sería algo como “Si lo puede hacer(condición), entonces lo hago (bloque condicional)”. El ejemplo arriba es más bien algo como “si lo hago todo, entonces bien”.

Conclusión

Hemos visto que la comprobación de errores puede afectar bastante a la claridad del código sobre el fin principal de una función. Dejando aparte la filosofía de una programación estructurada estricta podemos conseguir a reducir el nivel de anidamiento y separar el código en los pasos principales del algoritmo.

Referencias

El ejemplo de la “solución curiosa” fue inspirado en el código fuente de una implementación del algoritmo BSDIFF. Ver, por ejemplo, la función main del fichero bsdiff.cpp, líneas 260 a 264. Para descargar el archivo zip, pincha aquí.

Lectura adicional

Otras palabras claves
Técnicas el programación
Código más bonito

Escribe tu dirección de correo electrónico para suscribirte a este blog, y recibir notificaciones de nuevos mensajes por correo.

Únete a otros 48 seguidores

Archivos

agosto 2017
L M X J V S D
« Ene    
 123456
78910111213
14151617181920
21222324252627
28293031