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]


5.3.2a2  Retrollamadas

§1  Introducción

Las deficiencias, ya comentadas, del sistema de almacenamiento de usuario de los iostreams ( 5.3.2a1), motivó la inclusión de un dispositivo complementario. Básicamente consiste en que el usuario puede definir una serie de funciones auxiliares, que serán invocadas espontáneamente por el compilador cuando se presente alguna de las circunstancias potencialmente problemáticas. La idea es que el usuario incluya en estas funciones las medidas cautelares pertinentes.

Este esquema de funcionamiento, generalmente conocido como de retrollamadas "callbacks", es de utilización muy frecuente en informática. En lo que sigue lo identificaremos indistintamente por la palabra española o su equivalente inglesa.

§2 Mecanismo de retrollamadas

El artificio de las retrollamadas se compone de tres elementos, definidos como miembros públicos en ios_base:

enum event { erase_event, imbue_event, copyfmt_event };

typedef void (*event_callback)(event, ios_base&, int index);

void register_callback(event_call_back fn, int index);

event.  Una enumeración ( 4.8) que define tres constantes: erase_event, imbue_event y copyfmt_event.  Sus valores concretos dependen del compilador, pero pueden ser usadas mediante una interfaz estandarizada (los identificadores citados).  Como sus propios nombres indican, se refieren a tres circunstancias o "eventos" que pueden ocurrir en el programa. En nuestro caso, son precisamente los eventos que pueden suponer un riesgo potencial para el sistema iword/pword/xalloc descrito en el capítulo anterior. La primera, se refiere a la destrucción de un objeto iostream.  La segunda, a la invocación del método ios_base::imbue().  La tercera corresponde a la invocación basic_ios::copyfmt().

event_callback.  Un typedef ( 3.2.1a); en realidad, una forma abreviada de designar un puntero-a-función que devuelve void y recibe tres argumentos: una constante de enumeración tipo event, una referencia a un iostream y un entero que, como indica su nombre, actúa de índice o señalador.

register_callback.  Un método que devuelve void aceptando un event_callback (puntero-a-función) y un entero.

Como se habrá figurado el lector, los elementos de la interfaz "callback" son los necesarios para registrar una función y un entero (que será utilizado más tarde en la invocación de la función). La función puede ser cualquiera definida por el usuario, con dos condiciones:

  • Que pueda ser señalada por un event_callback. Es decir, que debe devolver void y recibir los argumentos indicados.

  • Que no lance ninguna excepción. En consecuencia, su declaración debería responder al siguiente prototipo ( 1.6.4):

void foo(ios::event e, ios_base& stream, int i) throw();  // §2a


Su funcionamiento es el siguiente: Cada iostream del programa puede disponer de su propia colección de funciones registradas. El registro se realiza invocando el método register_callback con el puntero a la función que deseamos registrar, y un entero que es precisamente un índice idx (aquí está el detalle) utilizado con iword() o pword() para almacenar datos de usuario en el iostream.

Nota:  Además de constituir la esencia del mecanismo de las retrollamadas, esto de "registrar" una función para que sea invocada cuando se presenta alguna circunstancia, es una técnica frecuentísima de programación. El propio lenguaje C++ dispone de algunas que ya hemos comentado. Por ejemplo, la función atexit() que sirve para "registrar" una función que será invocada antes de la terminación del programa ( 1.5.1).


Cuando en un objeto-flujo (un iostream) se presenta un evento de alguno de los tipos definidos en ios::event. Es decir, se realiza una invocación a alguno de los métodos: ios_base::~ios_base();  ios_base::imbue() o basic_ios::copyfmt(), el compilador realiza una comprobación de las funciones registradas para ese objeto y procede a su invocación (en orden inverso al de registro) proporcionando los tres argumentos como sigue (siguiendo con la nomenclatura utilizada en §2a):

  • ios::event e. Indicación del evento que ha originado el proceso.  Puede ser una Invocación al destructor, la inserción de un nuevo localismo o copia de su estado.  Generalmente este argumento permite a la función decidir "que hacer" en función de "que ha pasado".

  • ios_base& stream. El propio objeto (*this). Este argumento permitirá a la función realizar manipulaciones en el objeto adecuado (el que ha originado la invocación).

  • int i. El índice utilizado en el registro. Este argumento permite conocer que "slot" del parray del objeto debe utilizarse ( 5.3.2a1).

A tenor de lo anterior, es fácil deducir que estas invocaciones realizadas por el compilador [1], responden a alguna de las siguientes formas:

(*fn)(erese_event, *this, indx);

(*fn)(imbue_event, *this, indx);

(*fn)(copyfmt_event, *this, indx);

§3 Ejemplo

Como ejemplo de uso consideremos la forma de perfeccionar el caso expuesto en el capítulo anterior dándole forma de ejecutable [2]:

#include<iostream>

#include<fstream>

#include<string>

using namespace std;

 

static int indx = 0;  // en realidad static es aquí redundante

void foo1(char* fi);

void foo2(std::ifstream& st);

 

int main ( ) {   // =======================

    ...

    foo1("somefile.txt");

    ...

    ...          // quizás más invocaciones a foo1

}

 

void foo1(char* file) {

   ifstream streamI (file);

   foo2(streamI);

    ...

}

 

void foo2(std::ifstream& stream) {

   indx = std::ios_base::xalloc();

   (std::string*) stream.pword(indx) = new std::string[10];

   *((std::string*) stream.pword(indx)) = "Hola MUNDO";

}

El programa es lo suficientemente simple para ser abarcado de un vistazo. Dispone de dos funciones auxiliares; la primera crea un flujo de lectura streamI para realizar alguna computación (utilizando la terminología tradicional diríamos que abre un fichero de disco), pero antes de cualquier otra actividad, asocia al parray de dicho flujo cierta información. La asociación se realiza mediante una invocación a la segunda función foo2.  El detalle del cuerpo de esta última, que utiliza el binomio xalloc/pword, ha sido extensamente comentado en el capítulo anterior ( 5.3.2a1).

Recordando lo indicado sobre el funcionamiento de xalloc/pword, el problema de este tipo de diseño es que cada invocación de foo1 produce una pérdida del espacio reservado en  el montón (sentencia new en foo2), ya que al salir de ámbito, se destruye automáticamente el objeto streamI, y por consiguiente su parray y la información en él contenida (puntero al objeto string del montón).

Aunque podrían arbitrarse otras medidas, la solución propuesta por el Estándar es utilizar el mecanismo de "callback". Para esto registraremos una función foo3 que se encargará de desasignar el espacio del montón antes que sea destruido el flujo. El registro lo realizaremos en foo2, al que solo hay que añadir dos sentencias:

void foo2(std::ifstream& stream) {

    indx = std::ios_base::xalloc();

    (std::string*) stream.pword(indx) = new std::string[10];

    *((std::string*) stream.pword(indx)) = "Hola MUNDO";

    void (*pfoo)(ios::event, ios_base&, int) = &foo3;

    stream.register_callback(pfoo, indx);

}

la primera de ellas define un puntero-a-función pfoo de las características apropiadas, y lo inicia con la dirección de la nueva función (que comentamos a continuación). La sentencia siguiente es la que realiza la "inscripción" propiamente dicha de foo3. Observe que, como segundo argumento, utiliza el valor indx obtenido con xalloc y empleado en la invocación de pword.  También, que el registro de la función se realiza "en" el flujo que queremos controlar.

Por su parte el diseño de foo3 es de lo más sencillo:

void foo3(ios::event ev, ios_base& strm, int id) {

    if (ev == ios::erase_event) {

        cout << "El flujo será destruido" << endl;

        cout << "se procede a desasignar el espacio asociado" << endl;

        delete [] (std::string*) strm.pword(id);

    } else if (ev == ios::imbue_event) {

        cout << "Se ha imbuido un nuevo locale." << endl;

       // posibles medidas al respecto...

    } else if (ev == ios::copyfmt_event) {

        cout << "Atención los índices dejarán de ser válidos" << endl;

       // posibles medidas al respecto...

    }

}

El secreto para comprender su funcionamiento está en recordar que foo3 será invocada automáticamente cuando ocurra en el flujo alguno de los eventos ios::event.  En nuestro caso, al salir de ámbito la función foo1, momento en que es invocado el destructor de sus objetos automáticos, entre ellos el de streamI. Antes que el objeto sea destruido, el compilador invoca foo3 con los argumentos pertinentes al caso; entre ellos el tipo de evento ocurrido.

La inspección del cuerpo de foo3 no requiere mayor comentario, salvo quizás la sintaxis de la sentencia delete por la que desasignamos el espacio del montón. En nuestro caso, al salir de ámbito foo1 aparecerán en pantalla los mensajes pertinentes.

  Inicio.


[1]  Cuando en esta obra se dice que "el compilador" hace tal o cual cosa, refiriéndonos a sucesos de "runtime", debe entenderse que es una forma familiar (e informal) de referirse a actuación de mecanismos imbuidos en el ejecutable. Esta actuación sucede de forma automática, sin que el programador haya tomado precauciones especiales al respecto. Por ejemplo: "Después de las rutinas de inicio el compilador invoca la función main..."

[2]  Como se ha comentado repetidamente, la información de usuario en los iostreams suele referirse a datos de formato. Generalmente en construcciones muy sofisticadas que permiten opciones de formato distintas en cualquier flujo del programa. Por razones de simplicidad, este ejemplo no pretende ser reflejo de tales técnicas, sino del mecanismo que las sustenta.