You are currently browsing the tag archive for the ‘punteros’ 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

Punteros a funciones son una herramienta potente para agilizar el flujo de un programa. ¡Y una forma eficaz de perderse en el código!

El concepto de punteros o «referencias» a funciones existe en muchos lenguajes de programación. Por ejemplo, en Java Script, se puede asignar una función a una variable miembro de un objeto. Esta variable representa entonces la función asignada. En C y C++, estas variables deben tener, además, el tipo de la función a que apuntan. Restringir el tipo de la función reduce el número de posibles errores a la hora de ejecución y los incrementa a la hora de compilar el código.

En este artículo nos enfocamos a punteros de funciones en C y C++ y asumimos que el lector ya está familiarizado con las diferentes opciones de declarar const a variables y punteros, como se describen en el artículo
Como usar const en C++
.

La declaración de un tipo de función

Comenzemos con un ejemplo. ¿Quál es el tipo de la siguiente función?

int una_función(char parámetro);

La solución es

int (*puntero_a_una_función)(char)

Para extraer el tipo de la declaración de función hemos hecho lo siguiente:

  1. Hemos eliminado los nombres de los parámetros.
  2. Hemos puesto un asterisco delante el nombre del tipo y hemos puesto todo esto entre paréntesis.
  3. Hemos reemplazado el nombre de la función por el nombre puntero_a_una_función.

Echemos un vistazo más detallado.

Los nombres de los parámetros ya no aparecen. El tipo de una función es el mismo, si el valor de retorno y si los parámetros tienen el mismo tipo. Los nombres que tienen no influyen. El nombre de la función desaparece también. El nombre de la función juega un papel similar como un valor constante para una variable. El tipo de la variable no depende del nombre de una constante.

El asterisco indica que el tipo es un puntero. Lo ponemos entre paréntesis por la preferencia de operadores del lenguaje. Sin ellas habría un error de sintáxis, porque la llamada de función (paréntesis tras un identificador) tiene mayor prioridad que el operador asterisco.

Finalmente hemos cambiado el nombre. Decir más correctamente, hemos introducido un nuevo nombre. Este nuevo nombre puntero_a_una_función representa un tipo que puede guardar la dirección de una función con el tipo especificado.

Como el uso de tipos de funciones es poco legible, se suele casi siempre definir un tipo de función mediante un typedef. Esto lo hacemos aquí también.

int una_función(char parámetro);

// Definición de una variable dirección_de_una_función
int (*dirección_de_una_función)(char) = NULL;

// Definición de un tipo con typedef.
typedef int (*TIPO_DE_UNA_FUNCIÓN)(char);

// Definición de una variable con el tipo de typedef
// Se inicializa la variable con la dirección de una_función.
TIPO_DE_UNA_FUNCIÓN otra_dirección = una_función;

Por cierto, algo así no se puede hacer:

// Error
int (*dirección_de_una_función)(char) =
    int una_función(char parámetro);

Esta línea haría algo como delcarar una_función y asignar su dirección al mismo instante. Esto no está permitido.

Usar un puntero de función

Tras saber expresar el tipo de una función nos interesa usarlo. Lo podemos usar directamente para definir una variable.

// Una función que ya conocemos.
int una_función(char parámetro);

// Definimos nuestro tipo de función y una variable.
typedef int (*TIPO_DE_UNA_FUNCIÓN)(char);
TIPO_DE_UNA_FUNCIÓN puntero_a_una_función = una_función;

// El valor de una variable se puede asignar a otra.
TIPO_DE_UNA_FUNCIÓN otro_puntero = puntero_a_una_función;

// Se pueden comparar punteros de funciones,
// pero no se pueden aplicar las operaciones aritméticas 
// de los punteros a constantes y variables.
const bool igual = 
    puntero_a_una_función == otro_puntero; // Bien
puntero_a_una_función++; // Error de compilación

// Y, desde luego, podemos llamar a una función
// Se puede usar dos sintaxis diferentes
const char parámetro = 'a';
const int resultado = puntero_a_una_función(parámetro);
const int resultado2 = (*puntero_a_una_función)(parámetro);

Que se puede llamar a una función con dos sintáxis diferentes es un tanto peculiar, ya que suele haber una diferencia importante entre en un puntero y un puntero dereferenciado. Pero compilan los dos.

La pregunta es: ?cuál de las dos formas es la preferible? Pues, es cuestión de gusto. Yo prefiero la forma (*puntero_a_una_función)() ya que deja más claro que punter_a_una_función el el nombre de una variable y no de una función.

Como C y C++ permiten conversiones entre todo tipo de punteros, se pueden convertir también los punteros de funciones a punteros de variables.

// Obtenemos un puntero de variable a mi función.
int una_función(char parámetro);
char* puntero_a_datos = (char*)una_función;

// Con este puntero_a_datos no sólo puedes leer el código
// ejecutable de la función. También puedes modificarlo.
puntero_a_datos[5] = 0xFF;

Es obvio, que estas conversiones son ideales para hackers, pero será poco probable que se necesitan durante un uso normal del lenguaje. Por eso puedes esperar que el compilador te avisará con insistencia. Además, el código ejecutable de una función es de sólo lectura. O debería. Conseguir asignar algo a la memoria del código ejecutable es una manera excelente para introducir un error que no se encontrará hasta que alguien intente llamar a esta función.

Ejemplos con más clase

Punteros, referencias y constantes aparecen tal cual en un tipo de función.

// Declaración de una función con const y referencia.
MiClase *const devuelve_mi_instancia(const std::string& nombre,
                                     int* puntero);
                                     
// Definición de una variable puntero_a_devuelve_mi_instancia
MiClase *const (*puntero_a_función)(const std::string&, int*);

// Mejor definir un tipo con typedef
typedef MiClase *const (*TIPO_FUNC)(const std::string&, int*);
TIPO_FUNC otro_puntero_a_función;

// Los punteros a funciones también pueden ser constantes
MiClase *const (*const ptr_const)(const std::string&, int*) =
    puntero_a_función;

// Entonces ya no se puede asignar otra función.
ptr_const = NULL;  // Error

También se pueden llamar a los métodos estáticos de una clase. Tienen el mismo tipo que funciones globales.

class MiClase
{
    // Si el método no es público, no lo podemos acceder desde
    // fuera.
public:
    // Declaración de un método estático sin parámetro
    static int método_estático(void);
};

// El tipo del primer método. Este método no tiene parámetros.
typedef int (*TIPO_MÉTODO)();

// Al contrario de una función global
// la dirección de un método de clase se extrae con ampersand
TIPO_MÉTODO puntero_a_método = &MiClase::método_estático;

// Se puede llamar como una función global
const int resultado = (*puntero_a_método)();

Conceptualmente, las funciones miembros no estáticos no tienen dirección sin una instancia de la clase. Por eso se deben referenciar con un objeto. Físicamente el mismo método se encuentra a la misma dirección para todos los objetos. No obstante, el valor del puntero this es distinto.

class MásClase
{
public:
    // Declaración de un método normal
    float método_normal(int);
    
    // Declaración de un método constante
    float método_const(int) const;
};

// El tipo para métodos no estáticos
typedef float (MásClase::*TIPO_VAR)(int);
typedef float (MásClase::*TIPO_CONST)(int) const;
TIPO_VAR puntero_a_método = &MásClase::método_normal;
TIPO_CONST puntero_a_método_const = &MásClase::método_const;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
MásClase objeto;
const int parámetro = 5;
float resultado = (objeto.*puntero_a_método)(parámetro);
resultado = (objeto.*puntero_a_método_const)(parámetro);

El TIPO_VAR permite también la asignación de un TIPO_CONST, igual como una variable permite la asignación de una constante.

Como ejemplo de ¡basta ya! terminamos con un puntero a un patrón de función miembro.

// Un patrón de clase
template 
class UnaClase
{
public:
    // Declaración de un patrón de método
    template 
    void método_template(ParamMétodo& salida);
};

// Definir una variable con el tipo del template
// El tipo del patrón del método no aparece salvo en los
// parámetros.
typedef void (UnaClase::*TIPO_TEMPLATE)(float&);
TIPO_TEMPLATE puntero_a_método_template =
    &UnaClase::método_template;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
UnaClase objeto;
float parámetro;
(objeto.*puntero_a_método_template)(parámetro);

Conclusión

En este artículo hemos tratado como se declaran y se usan punteros a funciones. No hemos hablado para qué se usan, ya que es otro tema relativamente largo.

En general es preferible no usar punteros a funciones cuando es posible porque complican el análisis del código: ya no se puede saber fácilmente el nombre de la función que se llama. En el depurado saldría más bien la dirección de memoria a que el puntero de función apunta.

Por eso preferible pensar en alternativas. Cuando se trata eligir entre pocas funciones conocidas a tiempo de compilación, se podría pensar en un switch que llama a estas funciones según una variable de estado. Quizá es menos elegante pero herramientas de análisis de código no se pierden tan fácilmente. En C++, además, se ofrecen «functores» – es decir clases que sobrecargan el operador (). Físicamente functores son objetos que se pueden asignar como variables. Por eso tienen un tipo bastante más legible que punteros a funciones.

Referencias

Lectura adicional

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

Únete a otros 61 suscriptores

Archivos

May 2024
L M X J V S D
 12345
6789101112
13141516171819
20212223242526
2728293031