5.3.3c Manipuladores
§1 Introducción
En realidad, el concepto de manipulador ("manipulator") es muy amplio; se refiere a herramientas, o mecanismos, preconstruidos para hacer determinadas cosas. En el contexto de las Librerías Estándar de E/S, el término se refiere a algoritmos que ayudan con el manejo de flujos, especialmente en el caso de E/S formateadas (también pueden concebirse manipuladores para flujos no-formateados). Aunque las operaciones pueden ser de cualquier tipo, generalmente se refieren a operaciones de formateo.
El ejemplo clásico es suponer que queremos modificar el aspecto de las salidas estándar a consola std::cout ( 5.3). En este caso, supondremos que se desean las representaciones estándar y en formato decimal (fijo) de un número:
double x = 9876543.210;
std::cout << x;
// salida por defecto
std::cout.flags(std::cout.flags() | std::ios::fixed);
std::cout << " ";
std::cout << x; // salida decimal
Salida:
9.87654e+06 9876543.210000
Aquí no es posible encadenar ambas salidas en una sentencia. Además, muchas operaciones de formateo son repetitivas y es deseable disponer de un algoritmo de sintaxis lo más simple posible para realizarlas. Esta es justamente la idea de los manipuladores: que realicen la operación deseada y que puedan ser utilizados en inserciones o extracciones encadenadas.
§2 Manipuladores definidos por el usuario
Para ilustrar como podríamos implementar un manipulador propio; supongamos uno al que denominaremos decimal, que pueda efectuar la operación de cambio de formato del ejemplo anterior. Es decir, deseamos que una expresión del tipo:
std::cout << x << decimal << x <<;
nos proporcione la misma salida. Para ello utilizaremos la técnica de sobrecarga de operadores que vimos en el capítulo anterior ( 5.3.3b). Construiremos una clase realmente esquelética y un objeto igualmente vacío que nos ayuda en la operación. El código sería el siguiente:
class Foo { };
std::ostream& operator<< (std::ostream& stream, Foo foo) {
stream.flags(stream.flags() | std::ios::fixed);
return stream;
}
...
Foo decimal;
double x = 9876543.210;
std::cout << x << decimal << " " << x;
Salida:
9.87654e+06 9876543.210000
El truco aquí es que utilizamos una versión sobrecargada del operador << que acepta miembros de tipo Foo. En realidad, esta función operador no utiliza para nada el segundo argumento, que está simplemente para cubrir las exigencias sintácticas. Para más detalles sobre su funcionamiento puede repasar el referido epígrafe "Extractores e insertores de usuario".
Si piensa, y en esto coincidiríamos, que el recurso anterior no es demasiado elegante (definir una clase vacía y un objeto de dicho tipo en cada ámbito donde utilicemos el manipulador), puede utilizar un diseño alternativo consistente en definir una función con el nombre del manipulador que realice la tarea pertinente. Su aspecto sería el siguiente:
std::ostream& decimal (std::ostream& stream) {
stream.flags(stream.flags() | std::ios::fixed);
return stream;
}
La única condición es que la función devuelva una referencia a un iostream y acepte una referencia a dicho tipo. Observe que no es una función-operador; que solo recibe un argumento, y que no es necesario crear ningún objeto ni clase vacía. En estas condiciones ya es posible utilizar el identificador de la función como manipulador:
double x = 9876543.210;
std::cout << x << decimal << " " << x;
El secreto en este caso, es que los iostreams disponen de versiones sobrecargadas de los operadores de inserción y extracción, que aceptan como argumento un puntero-a-función como la utilizada en nuestro ejemplo. En concreto, ios_base dispone, entre otras, de una versión de << con el siguiente aspecto ( 5.3.3b):
basic_ostream<charT,traits>& operator<<(ios_base& (* pf)(ios_base&)); // §2a
y ante una invocación como.
cout.operator<<(decimal); // §2b
el compilador busca si existe una versión de operator<<() que acepte el tipo de decimal como argumento. En nuestro caso la concordancia se produce en la declaración §2a, de forma que se invoca el operador utilizando decimal como argumento.
Al llegar a este punto, quizás algún lector perspicaz piense que intento enredar las cosas deliberadamente. En efecto, una inspección detenida nos muestra que, el argumento exigido por §2a es puntero-a-función aceptando... y devolviendo... mientras que decimal es función aceptando... y devolviendo.... (ambos tipos no coinciden). Por si esto fuera poco, añadimos a continuación que "se invoca el operador utilizando decimal como argumento", cuando nos han enseñado hasta la saciedad que la gramática de C++ no permite utilizar funciones como argumento de otras funciones... y decimal es una función!!.
Nuevamente debemos encontrar la explicación en las sutilezas diabólicas del lenguaje. A pesar de toda esa palabrería rimbombante: "La gramática C++ no permite utilizar funciones como argumento de otras funciones...", aquí se cumple una vez más aquello (tan latino) de "hecha la ley, hecha la trampa", de forma que, para permitir pasar funciones como argumento de funciones, sin que lo anterior deje de ser cierto [1], nos inventamos una regla del siguiente tenor: "la invocación de una función mediante su puntero puede escribirse como si el puntero fuese el nombre de la función" ( 4.2.4b). Dicho con otras palabras: para casi todos los efectos, las funciones y sus punteros son intercambiables. Como consecuencia, el compilador no tiene absolutamente ningún escrúpulo en suponer que el tipo de decimal encaja perfectamente con el exigido por §2a, de forma que se busca un puntero-a-función... y se encuentra un puntero-a-función...
La segunda parte es la invocación; es necesario que se realice una invocación a decimal() -como función- con el argumento adecuado!. Es decir, §2b debe ser transformada en:
decimal(cout); // §2c
Esta parte del trabajo es realizado por la Librería de E/S. La definición del método §2a de ios_base incluye una invocación a la función señalada por el puntero utilizando *this como argumento ( 4.11.6).
basic_ostream<charT,traits>& operator<<(ios_base& (* pf)(ios_base&)) {
return (*pf) (*this);
}
Observe que, desde la óptica
de su empleo en expresiones como las anteriores, los manipuladores
pueden ser considerados como objetos que son insertados o extraídos de los flujos, para realizar determinadas manipulaciones
en ellos. Frecuentemente estas manipulación no suponen la "inserción" o
"extracción" de ningún carácter en la secuencia. Son meros
modificadores de las condiciones del flujo. Tampoco aportan ninguna
funcionalidad que no exista previamente, su aportación es simplemente comodidad sintáctica.
§2.1 Manipuladores con argumento
Aparte de los manipuladores descritos en el párrafo anterior, que se utilizan "tal cual", también es posible definir manipuladores que reciban un argumento, lo cual puede ser útil cuando deben establecer un parámetro de formato que puede ser variable. Como ejemplo, construyamos un manipulador, al que denominaremos decimals, que pueda ser utilizado como una función con el número de decimales (precisión) que queremos en la representación. Deseamos que una expresión como:
std::cout << decimals(4) << x;
proporcione el valor en formato decimal de x con 4 cifras decimales.
En este caso creamos una clase decimals con un constructor explícito, que es en realidad un constructor de conversión ( 4.9.18k), y la correspondiente versión sobrecargada del operador << que acepte un tipo de la nueva clase:
class decimals {
public:
std::streamsize d;
explicit decimals(std::streamsize d = 2) : d(d) {}
};
std::ostream& operator<< (std::ostream& stream, decimals d) {
stream.flags(stream.flags() | std::ios::fixed);
stream.precision(d.d);
return stream;
}
...
double x = 9876543.210;
std::cout << decimals() << x << "; " << decimals(9) << x;
Salida:
9876543.21; 9876543.210000001
Observe que los operandos decimals() y decimals(9) son sendos objetos anónimos de la clase decimals creados por el constructor, y que este dispone de un valor por defecto para su argumento.
[1] Inevitablemente me vienen a la mente ciertas artes de los políticos... y lo peor es que al final todos somos iguales!! Programadores y políticos :-((