5.5. Almacenamiento de iostream

Las buenas prácticas de diseño dictan que, cuando cree una nueva clase, debe esforzarse en ocultar los detalles de la implementación subyacente tanto como sea posible al usuario de la clase. Usted le muestra solo aquello que necesita conocer y el resto se hace privado para evitar confusiones. Cuando usamos insertadores y extractores, normalmente usted no conoce o tiene cuidado con los bytes que se consumen o se producen, ya que usted está tratando con E/S estándar, ficheros, memoria, o alguna nueva clase o dispositivo creado.

Llega un momento, no obstante, en el que es importante comunicar con la parte del iostream que produce o consume bytes. Para proveer esta parte con una interfaz común y esconder todavía su implementación subyacente, la librería estándar la abstrae dentro de su clase, llamada streambuf. Cada objeto iostream contiene un puntero a alguna clase de streambuf. (El tipo depende de que se esté tratando con E/S estándar, ficheros, memoria, etc.). Puede acceder al streambuf directamente; por ejemplo, puede mover bytes sin formatear dentro y fuera del streambuf sin formatearlos a través de la encapsulación del iostream. Esto es posible llamando a las funciones miembro del objeto streambuf.

Actualmente, la cosa más importante que debe conocer es que cada objeto iostream contiene un puntero a un objeto streambuf, y el objeto streambuf tiene algunas funciones miembro que puede llamar si es necesario. Para ficheros y streams de string, hay tipos especializados de buffers de stream, como ilustra la figura siguiente:

Para permitirle el acceso al streambuf, cada objeto iostream tiene una función miembro llamada rdbuf() que retorna el puntero a un objeto streambuf. De esta manera usted puede llamar cualquier función miembro del streambuf subyacente. No obstante, una de las cosas más interesantes que usted puede hacer con el puntero al streambuf es conectarlo con otro objeto iostream usando el operador <<. Esto inserta todos los carácteres del objeto dentro del que está al lado izquierdo del <<. Si quiere mover todos los carácteres de un iostream a otro, no necesita ponerse con el tedioso (y potencialmente inclinado a errores de código) proceso de leer de carácter por carácter o línea por línea. Este es un acercamiento mucho más elegante.

Aqui está un programa muy simple que abre un fichero y manda el contenido a la salida estándar (similar al ejemplo previo):

//: C04:Stype.cpp
// Type a file to standard output.
#include <fstream>
#include <iostream>
#include "../require.h"
using namespace std;

int main() {
  ifstream in("Stype.cpp");
  assure(in, "Stype.cpp");
  cout << in.rdbuf(); // Outputs entire file
} ///:~

Listado 5.6. C04/Stype.cpp


Un ifstream se crea usando el fichero de código fuente para este programa como argumento. La función assure() reporta un fallo si el fichero no puede ser abierto. Todo el trabajo pasa realmente en la sentencia

cout << in.rdbuf();

que manda todo el contenido del fichero a cout. No solo es un código más sucinto, a menudo es más eficiente que mover los byte de uno en uno.

Una forma de get() escribe directamente dentro del streambuf de otro objeto. El primer argumento es una referencia al streambuf de destino, y el segundo es el carácter de terminación ('\n' por defecto), que detiene la función get(). Así que existe todavía otra manera de imprimir el resultado de un fichero en la salida estándar:

//: C04:Sbufget.cpp
// Copies a file to standard output.
#include <fstream>
#include <iostream>
#include "../require.h"
using namespace std;

int main() {
  ifstream in("Sbufget.cpp");
  assure(in);
  streambuf& sb = *cout.rdbuf();
  while(!in.get(sb).eof()) {
    if(in.fail())          // Found blank line
      in.clear();
    cout << char(in.get()); // Process '\n'
  }
} ///:~

Listado 5.7. C04/Sbufget.cpp


La función rdbuf() retorna un puntero, que tiene que ser desreferenciado para satisfacer las necesidades de la función para ver el objeto. Los buffers de stream no estan pensados para ser copiados (no tienen contructor de copia), por lo que definimos sb como una referencia al buffer de stream de cout. Necesitamos las llamadas a fail() y clear() en caso de que el fichero de entrada tenga una línea en blanco (este la tiene). Cuando esta particular versión sobrecargada de get() vee dos carácteres de nueva línea en una fila (una evidencia de una línea en blanco), activa el bit de error del stream de entrada, asi que se debe llamar a clear() para resetearlo y que así el stream pueda continuar siendo leído. La segunda llamada a get() extrae y hace eco de cualquier delimitador de nueva línea. (Recuerde, la función get() no extrae este delimitador como sí lo hace getline()).

Probablemente no necesitará usar una técnica como esta a menudo, pero es bueno saber que existe.[15]



[15] Un tratado mucho más en profundidad de buffers de stream y streams en general puede ser encontrado en[ Langer & Kreft's, Standar C++ iostreams and Locales, Addison-Wesley, 1999.]