¿Cómo funcionan los manipuladores de flujo?

Es bien sabido que el usuario puede definir manipuladores de flujo como este:

ostream& tab(ostream & output) { return output<< '\t'; } 

Y esto se puede usar en main () de esta manera:

 cout<<'a'<<tab<<'b'<<'c'<<endl; 

Por favor, explícame cómo funciona todo esto? Si el operador << asume como un segundo parámetro un puntero a la función que toma y devuelve ostream & , entonces explique por qué es necesario? ¿Qué sería incorrecto si la función no toma y devuelve ostream pero fue nula en lugar de ostream & ?

También es interesante por qué los manipuladores “dec”, “hex” entran en vigencia hasta que no cambio entre ellos, pero los manipuladores definidos por el usuario siempre deben usarse para tener efecto para cada transmisión.

El estándar define la siguiente operator<< sobrecarga del operator<< en la plantilla de la clase basic_ostream :

 basic_ostream& operator<<( basic_ostream& (*pf) (basic_ostream&) ); 

Efectos: ninguno No se comporta como una función de salida formateada (como se describe en 27.6.2.5.1).

Devuelve: pf(*this) .

El parámetro es un puntero a una función que toma y devuelve una referencia a std::ostream .

Esto significa que puede "transmitir" una función con esta firma a un objeto ostream y tiene el efecto de llamar a esa función en la transmisión. Si usa el nombre de una función en una expresión, entonces (generalmente) se convierte en un puntero a esa función.

std::hex es un manipulador std::ios_base definido de la siguiente manera.

  ios_base& hex(ios_base& str); 

Efectos: Llama a str.setf(ios_base::hex, ios_base::basefield) .

Devoluciones: str.

Esto significa que la transmisión de hex a un ostream configurará los indicadores de formato de la base de salida para generar números en hexadecimal. El manipulador no genera nada en sí mismo.

No tiene nada de malo, excepto que no hay un operador << sobrecargado definido para él. Las sobrecargas existentes para << esperan un manipulador con la firma ostream & (* fp) (ostream &) .

Si le diera un manipulador con el tipo ostream & (* fp) () , obtendría un error de comstackción ya que no tiene una definición para el operador << (ostream &, ostream & (* fp) ()) . Si desea esta funcionalidad, deberá sobrecargar el operador << para aceptar manipuladores de este tipo.

Debería escribir una definición para esto:
ostream & ostream :: operator << (ostream & (* m) ())

Tenga en cuenta aquí que nada mágico está sucediendo aquí. Las bibliotecas de flujo dependen en gran medida de las características estándar de C ++: sobrecarga del operador, clases y referencias.

Ahora que sabe cómo puede crear la funcionalidad que describió, aquí es por qué no:

Sin pasar una referencia a la transmisión que intentamos manipular, no podemos realizar modificaciones en la transmisión conectada al dispositivo final (cin, out, err, fstream, etc.). La función (los modificadores son solo funciones con nombres extravagantes) tendría que devolver un nuevo ostream que no tuviera nada que ver con el que está a la izquierda del operador <<, o mediante un mecanismo muy feo, descubrir qué ostream debería Conéctate con else todo a la derecha del modificador, no llegará al dispositivo final, sino que más bien se enviará a lo que ostream devuelva la función / modificador.

Piensa en transmisiones como esta

 cout << "something here" << tab << "something else"<< endl; 

realmente significa

 (((cout << "something here") << tab ) << "something else" ) << endl); 

donde cada conjunto de paréntesis hace algo para cout (escribir, modificar, etc.) y luego devuelve cout para que el siguiente conjunto de paréntesis pueda trabajar en él.

Si su modificador / función de tabulación no tomó una referencia a un ostream, tendría que adivinar de alguna manera qué era ostream a la izquierda del operador << para realizar su tarea. ¿Estabas trabajando con cour, cerr, alguna secuencia de archivos ...? Las partes internas de la función nunca lo sabrán a menos que se les entregue esa información de alguna manera, y por qué no que sea tan simple como una referencia a la misma.

Ahora, para realmente aclarar el punto, veamos qué es endl realmente y qué versión sobrecargada del << operador que estamos usando:

Este operador se ve así:

  ostream& ostream::operator<<(ostream& (*m)(ostream&)) { return (*m)(*this); } 

endl se ve así:

  ostream& endl(ostream& os) { os << '\n'; os.flush(); return os; } 

El objective de endl es agregar una nueva línea y vaciar la secuencia, asegurándose de que todo el contenido del buffer interno de la secuencia se haya escrito en el dispositivo. Para hacer esto, primero necesita escribir un '\ n' en esta secuencia. Luego necesita decirle a la stream que descargue. La única manera de que endl sepa qué flujo escribir y descargar es para que el operador pase esa información a la función endl cuando lo llame. Sería como que te diga que laves mi coche, pero nunca te digo qué coche es el mío en el estacionamiento completo. Nunca serás capaz de hacer tu trabajo. Necesitas que te dé mi auto o puedo lavarlo yo mismo.

Espero que eso aclare las cosas

PD: si accidentalmente encuentras mi coche, lávalo.

Normalmente, el manipulador de flujo establece algunos indicadores (u otras configuraciones) en el objeto de transmisión, de modo que la próxima vez que se use, actuará de acuerdo con los indicadores. Por lo tanto, el manipulador devuelve el mismo objeto que pasó. La sobrecarga del operator<< que llamó al manipulador ya tiene este objeto, por supuesto, de modo que, como ha notado, el valor de retorno no es estrictamente necesario para ese caso. Creo que esto cubre todos los manipuladores estándar, todos devuelven sus comentarios.

Sin embargo, con el valor de retorno, el marco es lo suficientemente flexible como para que un manipulador de flujo personalizado pueda devolver un objeto diferente, presumiblemente un contenedor para el objeto dado. Este otro objeto se devolverá desde la cout << 'a' << tab , y podría hacer algo que la configuración de formato ostream no sea compatible.

Sin embargo, no estoy seguro de cómo haría para que este otro objeto sea liberado, así que no sé qué tan práctico es esto. Puede tener que ser algo peculiar, como un objeto proxy que es administrado por el propio ostream . Entonces, el manipulador solo funcionaría para las clases de flujo personalizado que lo soportan activamente, que no suele ser el objective de los manipuladores.