Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


Punteros a funciones: aclaración y ejemplo

§1 Sinopsis

Al tratar de la sintaxis de la declaración de puntero-a-función (sección 4.2.4a), hemos señalado que en las expresiones:

int (*fptr)(int);   // L.1
int  *fptr (int);   // L.2

L.1 es la declaración de un puntero-a-función que recibe un int y devuelve un int. Mientras que L.2 es el prototipo de una función que recibe un int y devuelve puntero-a-int. En consecuencia:

void func(int (*fptr)(int));   // L.3: Ok.
void func(int *fptr(int));     // L.4: Ilegal!!

La expresión L.3 es la declaración (prototipo) de una función que no devuelve ningún valor y recibe como argumento un puntero-a-función. Por su parte L.4 es una expresión ilegal, porque pretende definir una función que recibe como argumento a otra función. Sin embargo, es posible compilar sin error dicha expresión en cualquiera de los compiladores más utilizados en la informática personal,  Borland C++ 5.5 y MS Visual C++ 6.0, lo que parece estar en contradicción con el hecho de que C/C++ no permiten utilizar funciones como argumentos de otras funciones.

Estudiado el caso más de cerca, es fácil comprobar que en realidad una declaración como L.4 es interpretada por ambos compiladores como la declaración de una función que no devuelve nada y recibe como argumento un puntero-a-función que recibe un int y devuelve un puntero-a-int [1].

Nota: esta conversión entre el tipo declarado y el realmente utilizado por el compilador, se engloba dentro de las conversiones implícitas que sufren los argumentos actuales pasados a funciones, que es característica de los compiladores C/C++ y se realiza automáticamente cada vez que es necesario y posible [4] ( 2.2.5).

§2 Ejemplo

Como demostración y repaso, exponemos un ejemplo que utiliza los principales conceptos relacionados con los punteros-a-función:

#include <iostream>
using namespace std;

int* fun0(int i) {              // L.4
  cout << i << endl;
  return &i;
}
int fun1(int i) {               // L.8
  cout << i << endl;
  return 10*i;
}

void fun2(int *pf(int)) { pf(3); }          // L.13
void fun3(int (*pf)(int)) { pf(20); }       // L.14

int main(void) {                // =========
  int x = 10;
  int y = fun1(x);              // M.3
  fun1(fun1(y));                // M.4
  int (*pf1)(int) = &fun1;      // M.5
  pf1(x);                       // M.6
  fun3(pf1);                    // M.7
  int* (*pf2)(int) = &fun0;     // M.8
  pf2(y);                       // M.9
  fun2(pf2);                    // M.10

  fun2(fun0);                   // M.11
  return 0;
}

Salida:

10
100
1000
10
20
100
3
3

Comentario

L.4: definición de una sencilla función que recibe un int y devuelve un puntero-a-int [2]

L.8: función que recibe un int y devuelve un int.

L.13: Función problemática. Supuestamente función que no devuelve nada y recibe una función que recibe un int y devuelve un puntero-a-int (ver L.4).

L.14: Función que no devuelve nada y recibe un puntero-a-función que recibe un int y devuelve un int. (ver L.1 de la sinopsis ).  Observe que tanto esta como la anterior, ejecutan la función señalada por su argumento [3].

M.3: Definimos un entero y, igualándolo al valor devuelto por la fun1 definida en L.8; esta sentencia produce la primera salida del programa.

M.4: En esta sentencia, responsable de la segunda y tercera salidas, se muestra el resultado de una invocación recursiva a la fun1. La primera invocación (la interior), utiliza el valor y como argumento (un int). Observe que la segunda invocación (la exterior) no utiliza una función como argumento, en realidad utiliza un int, (el valor 1000 devuelto por la primera invocación).

M.5: Definición de pf1, un puntero-a-función que recibe un int y devuelve un int.  Lo igualamos a la dirección de la función fun1 (definida en L.8), que cumple las condiciones exigidas en la declaración.

M.6:  Invocamos la función func1 utilizando su puntero. Es la responsable de la salida 4.

M.7: Ejecutamos la función func3 definida en L.14, utilizando el argumento adecuado (pf1 cumple las condiciones exigidas). A su vez ejecuta la función fun1 señalada por el puntero. Es la responsable de la salida 5.

M.8: Definimos pf2 como puntero-a-función que recibe un int y devuelve un puntero-a-int.  Lo iniciamos con la dirección de la función fun0 (definida en L.4) que cumple con los requisitos exigidos.

M.9: Invocamos fun0 utilizando su puntero y el argumento adecuado. Es la salida 6.

M.10: Aquí está la comprobación del misterio: invocamos la función problemática (fun2), definida en L.13, utilizando el puntero pf2 como argumento. Es responsable de la salida 7, ya que ejecuta la función fun0 señalada por su argumento.

M.11:  Esta sentencia, responsable de la última salida, parece contradecir nuestra hipótesis, ya que aparentemente fun2 acepta aquí una función como argumento y proporciona una salida coherente. La razón es que en este caso, el compilador construye un objeto temporal de tipo adecuado: puntero-a-función que recibe un int y devuelve un puntero-a-int, lo iguala a la dirección de fun0 y lo utiliza como argumento pasado a la función.

Puede obtenerse una comprobación adicional de la verdadera naturaleza del argumento utilizado en fun2 modificando ligeramente dicha función:

void fun2(int *pf(int)) { cout << typeid(pf).name() << endl; } // L.13-bis

Ahora, el resultado de cada invocación a fun2 es el siguiente:

int * (*)(int)

Para el compilador, el tipo int* (*)(int) significa puntero-a-función que recibe un int y devuelve un puntero-a-int.  Es fácil comprobarlo añadiendo en el cuerpo de la función main un par de sentencias:

int* (*pfunc) (int);     // declaración de puntero-a-función ...

cout << typeid(pfunc).name() << endl;

La salida resulta igualmente:   

int * (*)(int)

Nota: el simbolismo utilizado para este tipo de salidas no está estandarizado, por lo que depende del compilador. El que se muestra corresponde a Borland C++ 5.5. En la página adjunta se muestra una comparativa con otros compiladores ( Designación de tipos en los compiladores 4.2.4a2).


Realizando la misma operación con la declaración de cualquier función F, vemos que para el compilador, su tipo es el mismo que el de un puntero-a-función-F (de su misma clase). La explicación es que en realidad, el compilador asimila cada función con un puntero-a-función-del-mismo-tipo, iniciado al punto de inicio de la función (lo que por otra parte tiene bastante sentido). Esto explica que la invocación de una función F se haga exactamente de la misma forma utilizando el nombre que utilizando su puntero. En el caso de ejemplo, las dos expresiones que siguen son exactamente equivalentes:

fun1(x);  // invocación de fun1 pasando x como argumento (utilizando el nombre)

pf1(x);   // idem idem (utilizando su puntero)

Nota: a pesar de lo dicho, el compilador puede distinguir perfectamente la verdadera naturaleza del identificador de una función F y del identificador de su puntero pF, pues existen otros atributos (adicionales al tipo) que acompañan a cada objeto. Por ejemplo, F es reconocido como un Rvalue ( 2.1), mientras que pF es un Lvalue ( 2.1). Esta diferencia hace no pueda realizarse una asignación sobre F y si sobre pF:

fun2 = &fun0;  // Error!! No permitido

pfunc =&fun0;  // Ok.

  Inicio.


[1]  Podríamos decir que desde esta óptica, el comportamiento de ambos compiladores es bastante deficiente, y que induce a confusión (sobre todo al principiante) acerca del tipo realmente utilizado en el argumento. En este caso el comportamiento correcto sería devolver un error de compilación.  Lo que ocurre en realidad es una conversión implícita de tipo, de función a puntero-a-función ( 2.2.5).

[2] Debe entenderse que hemos diseñado esta función con fines didácticos exclusivamente, ya que no es razonable utilizar el valor devuelto por este tipo de funciones.

[3] Esta invocación un tanto "extraña", de una función utilizando su puntero como si fuese la propia función. Se comenta más ampliamente en el apartado "Invocar funciones mediante punteros" ( 4.2.4b).

[4]  "Argument types are checked and implicit argument type conversión takes place when necesary.... The value of such checking and type conversion should not be underestimated". Stroustrup TC++PL §7.1.