5.3.2a1 Área de almacenamiento de usuario
§1 Introducción
En esta sección explicaremos las peculiaridades del sistema de almacenamiento de usuario en los objetos-flujo (iostreams). Sistema que está representado por tres métodos públicos de ios_base:
long& iword(int idx);
void*& pword(int idx);
static int xalloc();
Como tendremos ocasión de comprobar, el sistema original presentaba algunas deficiencias (facilidad de originar pérdidas de memoria), por lo que fue completado con un sistema de registro de funciones ("callbacks") para hacerlo más robusto, al que dedicamos la siguiente sección ( 5.3.2a2).
§2 Sinopsis
Cada objeto iostream dispone de un área de almacenamiento de usuario; este área es propia de cada flujo e independiente de las demás. Podemos imaginarla como una matriz, inicialmente vacía, que pueden crecer indefinidamente según las necesidades [1]. Esta matriz, conocida como parray del objeto, tiene un comportamiento realmente curioso [2]: Para empezar, permite almacenar indistintamente enteros tipo long, o punteros-a-void (genéricos 4.2.1d), de forma que el elemento parray[idx] puede ser de cualquiera de los tipos anteriores. Sus miembros idx son accedidos mediante las referencias devueltas por los métodos iwod() y pword(). Además, son extensibles, en el sentido de que pueden crecer indefinidamente. Para rematarlo, la validez de sus datos es de lo más singular.
La invocación iword(idx), devuelve una referencia a un long, que corresponde con el valor del elemento idx de esta matriz (el elemento iword[idx]). Si se pasa un argumento cualquiera n, que no haya sido visto antes, la parray crece automáticamente hasta alcanzar el tamaño correspondiente, e inicializa los nuevos elementos a cero, de forma que el valor devuelto es cero.
El método pword() tiene un comportamiento análogo, con la diferencia de que devuelve la referencia a un puntero genérico, lo que permite incluir una referencia a cualquier objeto. Por ejemplo, una constante de cadena. Sin embargo hay que ser precavido, porque el diseño de ios_base no garantiza la destrucción del objeto señalado cuando se destruye el flujo correspondiente (en realidad se encarga de la destrucción "del puntero", no del objeto señalado por él) [5].
En caso de fallo. Por ejemplo, por falta de almacenamiento, ambos métodos ponen a 1el bit de estado badbit ( 5.3.2a).
§2.1 Si queremos un índice que pueda ser utilizado en las invocaciones
iword()/pword() de cualquier iostream, entonces hay
que realizar una invocación a xalloc(). El valor (estático)
devuelto por este método puede ser utilizado como índice para las invocaciones de iwod()
y pword() en cualquier flujo. Cada invocación sucesiva a xalloc()
devuelve un índice distinto del anterior.
Nota: El trinomio iword/pword/xalloc está pensado para operar (y se complementa) con el método copyfmt() de basic_ios. Para que se cumpla la condición anterior: que el índice devuelto por xalloc() pueda ser utilizado con cualquier otro iostream, es necesario copiar el parray del flujo original en el nuevo. En el apartado correspondiente a copyfmt() se incluye un ejemplo aclaratorio ( 5.3.2b).
§2.2 Hay que hacer notar que, las invocaciones iword()/pword() que
hacen crecer parray, son irreversibles. En el sentido que los elementos creados no pueden ser rehusados
posteriormente. Respecto a la validez de los datos almacenados, la primera observación es que
el parray es eliminado en el proceso de destrucción del iostream
correspondiente. Además, las referencias devueltas para un índice idx
anterior, pueden no ser válidas en las circunstancias siguientes:
Después de una invocación a los métodos iword()/pword() del objeto con un índice distinto.
Después de una invocación al método copyfmt() del objeto.
Nota: La razón argüida es que, en ambos casos, puede ser necesaria una reconstrucción de las correspondiente áreas de almacenamiento y que los elementos no sean recolocados en la misma posición (con lo que los índices no serían ya válidos). El estándar advierte que "puede" que no sean válidos. En consecuencia, aunque el resultado obtenido con su compilador se correcto en estas circunstancias, si quiere construir una aplicación verdaderamente portable, debe asumir que no lo serán.
§3 Funcionamiento
Un esquema de uso de xalloc()/iword() podría ser el siguiente:
std::ifstream stream; // Un iostream cualquiera
...
static int indx = stream.xalloc(); // L.1
static long& valor = stream.iword(indx); // L.2
valor = stream.flags(); // L.3
Comentario:
En L.1 se obtiene un índice indx, que podrá ser utilizado con cualquier iostream del programa. El hecho de que hayamos decidido hacer indx estático, es que puede ser común para todos los iostreams, por lo que deseamos conservarlo (una nueva invocación a xalloc() producirá un índice distinto!!).
En L.2 se obtiene una referencia al elemento indx del área de almacenamiento numérico (matriz iword). Finalmente, en L.3 utilizamos este "slot" para guardar el formato actual de un flujo concreto [8].
Observe que suponiendo la existencia de la cabecera <ios>, el hecho de ser xalloc() un método estático, permite que pueda ser utilizado con anterioridad la existencia un flujo concreto ( 4.11.7):
static int indx = std::ios_base::xalloc(); // L.1
...
std::ifstream stream; // Un iostream cualquiera
static long& valor = stream.iword(indx); // L.2
valor = stream.flags(); // L.3
Igualmente, el hecho de ser una referencia el valor devuelto por iword(), permite que la asignación anterior pueda ser realizada sobre la invocación de la función ( 4.13.7):
static int indx = std::ios_base::xalloc(); // L.1
...
std::ifstream stream; // Un istream cualquiera
stream.iword(indx) = stream.flags(); // L.2+L.3
Si la información que deseamos asociar al iostream es algo más que lo que puede contener un long, entonces es necesario utilizar cualquier otro objeto; matriz, estructura, Etc. y referenciarla con el puntero devuelto por pword(). Este puntero puede ser perdurable y único (si ha sido obtenido mediante xalloc()), pero es nuestra responsabilidad hacer que el objeto (matriz, estructura, Etc) también lo sea, y recordar destruirlo antes de perder el puntero (o el índice con el que es accedido).
§4 Peligros potenciales
Generalmente el trinomio iword/pword/xalloc se utiliza para almacenar información relativa a los flujos, pero observe que en el caso de pword, la información es externa al flujo propiamente dicho, lo único que se guarda en él es un puntero a la información. Para la localización física de tales datos caben dos soluciones:
La primera posibilidad es utilizar un objeto automático, que será destruido en cuanto salga de ámbito. Situación esta que probablemente no nos interesa, ya que la verdadera razón de ser de este tipo de almacenamiento, es guardar información que pueda ser compartida por otros flujos del programa (actuales y futuros). La segunda posibilidad (la más probable) es que utilicemos un objeto persistente, del montón ( 1.3.2). Ambas posibilidades pueden ser esquematizadas como sigue:
static int indx = std::ios_base::xalloc();
...
void foo(std::istream& stream) {
std::string s1[10] = {"Hola MUNDO"};
(std::string*) stream.pword(indx) = s1;
...
}
Este primer supuesto comienza con una invocación a xalloc para obtener un índice estático indx. En foo se declara un objeto dinámico s1, un string de 10 caracteres, que es inicializado [7]. A continuación, se almacena la dirección de dicho objeto en el "slot" proporcionado por pword(). Para ello hemos efectuado un modelado, que transforma el tipo void* devuelto por pword() en el tipo adecuado (puntero-a-string). Observe que hemos aprovechado que la dirección de un array es equivalente a su identificador ( 4.3.2) para iniciar este puntero.
El problema aquí es que el objeto s1 es destruido cuando la ejecución salga de foo (quizás también el flujo stream). Si posteriormente otro flujo solicita el mismo puntero mediante pword(indx) la dirección devuelta corresponde a un objeto que ha sido destruido. El resultado es basura o un error fatal de runtime. Una opción alternativa es utilizar un objeto persistente:
static int indx = std::ios_base::xalloc();
...
void foo(std::istream& stream) {
(std::string*) stream.pword(indx) = new std::string[10];
*((std::string*) stream.pword(indx)) = "Hola MUNDO";
...
}
El segundo diseño comienza igualmente con una invocación a xalloc para obtener un índice, pero el objeto string es creado en el montón (se ha reservado el espacio correspondiente). El puntero devuelto por el operador new es asignado al "slot" proporcionado por pword() sobre el índice indx. La sentencia siguiente tiene por misión iniciar el objeto creado (para estar en igualdad de condiciones con el caso anterior). Observe que el Lvalue de esta última sentencia es un string (indirección de un puntero-a-string).
La nueva construcción presenta ventajas e inconvenientes; la ventaja es que, a pesar de que sea destruido el objeto stream, la invocación pword(indx) sobre cualquier otro flujo, seguirá proporcionando acceso al objeto string. La única precaución es utilizar copyfmt() para copiar el parray del flujo original stream en el nuevo antes que el primero desaparezca.
Supongamos ahora que un nuevo flujo nstream ya dispone de su propio parray copiado del anterior y que indx sigue disponible, pero que hemos añadimos alguna otra información por el mismo procedimiento: obtener un nuevo índice indx2 con xalloc() y la correspondiente invocación pword(indx2). El problema ahora es que los índices anteriores pueden no ser válidos , de forma que, por ejemplo, la sentencia
cout << *((std::string*) stream.pword(indx));
podría no proporcionar el esperado "Hola MUNDO". Otra posible fuente de error es que se destruyera el flujo inicial sin haber copiado el parray en ningún otro. En este caso también se perdería el espacio reservado en el montón, que resultaría definitivamente inaccesible. Para resolver este tipo de problemas, el mecanismo pword/iword/xalloc fue complementado con un dispositivo auxiliar que describimos en el epígrafe siguiente.
[1] Se trata de un miembro privado que es accedido mediante los métodos iword() y pword(). La implementación concreta puede variar, pero podemos imaginarlo como una matriz extensible de uniones ( 4.7). El hecho que sus miembros sean uniones permite que cada uno pueda contener indistintamente un long y un puntero genérico. Esta unión podría responder a la definición:
union ios_user_union {
long lword;
void* pword;
};
[2] Como podrá comprobar, la primera impresión del profano es pensar que, a los señores del Comité, se les hubiera escapado el asunto de las manos en algunos rincones de la librería (no dudo que tendrán sus razones). En cualquier caso, el comportamiento no deja de ser ciertamente sofisticado.
[5] Respecto a este asunto, es muy ilustrativo el comentario del mentado Matt Austern, presidente del grupo de trabajo de la Librería Estándar dentro del Comité de Estandarización del C++, en su artículo "The Standard Librarian: User-Defined Format Flags" en Cpp Users Journal, Febrero del 2000, que trascribimos literalmente, para no poner ni quitar nada de nuestra parte en tan autorizada opinión:
"I described pword for completeness. You shouldn't use it: it's one of the very most obscure and complicated corners of the C++ I/O library. It's hard to understand code that uses such features and easy to get it wrong, and with a little bit of thought, you can almost certainly come up with an alternative design that sidesteps the whole mess. None of the built-in I/O operators in the C++ Standard library use pword or anything like it. I haven't shown an example where pword is necessary, because most of the examples that I've seen seem a bit artificial.
[7] Un string es un contenedor, definido en la STL, para contener cadenas de caracteres. La clase dispone de versiones sobrecargadas de la mayoría de operadores, incluyendo los de inserción (<<) y extracción (>>), lo que permite una manipulación muy cómoda de cadenas de caracteres.
[8] Aquí me permito la licencia de utilizar la denominación inglesa "slot"; me parece más descriptiva que su equivalente hueco/ranura para este uso "informático" del significado.