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 una variable 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 ParamClase>
class UnaClase
{
public:
    // Declaración de un patrón de método
    template <class ParamMétodo>
    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<int>::*TIPO_TEMPLATE)(float&);
TIPO_TEMPLATE puntero_a_método_template =
    &UnaClase<int>::método_template<float>;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
UnaClase<int> 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