2012-01-11 36 views
13

Estoy pensando en un problema que tiene cierta similitud con el reenvío perfecto, pero donde el argumento de la función no se pasa a una función llamada, sino que se devuelve. Es por eso que lo llamo "paso perfecto".Paso perfecto

El problema es el siguiente:

Digamos que tenemos una función que toma un objeto por referencia (y, posiblemente, algunos argumentos extra), modifica ese objeto, y devuelve el objeto modificado. El ejemplo más conocido de tales funciones es probablemente operator<< y operator>> para iostreams.

Usemos iostreams como ejemplo, porque permite mostrar lo que estoy buscando. Por ejemplo, una cosa cuál sería veces gustaría hacer es:

std::string s = (std::ostringstream() << foo << bar << baz).str(); 

Por supuesto que no funciona, por dos razones:

  • std::ostringstream() es un valor de lado derecho, pero operator<< toma un valor izquierdo como primer argumento

  • operator<< devuelve un ostream& (bueno, al menos para los estándares en realidad un basic_ostream<CharT, Traits>& donde CharT y Traits se deducen del primer argumento).

Así que vamos a suponer que queremos diseñar el operador de inserción para que las obras anteriores (que, obviamente, no pueden hacer eso por los operadores existentes, pero se puede hacer que para sus propias clases). Es evidente que la solución debe tener las siguientes características:

  • El primer argumento puede aceptar un valor-I o un valor de lado derecho.

  • El tipo de devolución debe ser del mismo tipo que el que se entregó. Pero, por supuesto, solo debe aceptar ostreams (es decir, las clases derivadas de una instanciación de basic_ostream).

Si bien en este caso de uso específico que no es necesario, quiero añadir un tercer requisito:

  • Si el primer argumento es un valor de lado derecho, por lo que es el valor devuelto, de lo contrario un lvalue es regresado.

Esta regla adicional es para que pueda mover-construir desde un valor r pasado a través de la función (no sé si las secuencias de C++ 11 se pueden mover, pero es un esquema más general , con la transmisión como ejemplo útil).

Es obvio que en C++ 03, esos requisitos no podrían cumplirse. Sin embargo, en C++ 11, tenemos referencias rvalue, que deberían hacer esto posible.

Aquí está mi intento en esto:

#include <iostream> 
#include <sstream> 
#include <string> 

template<typename Ostream> struct is_ostream 
{ 
    typedef typename std::remove_reference<Ostream>::type candidate; 
    typedef typename candidate::char_type char_type; 
    typedef typename candidate::traits_type traits_type; 
    typedef std::basic_ostream<char_type, traits_type> basic_ostream; 
    static const bool value = std::is_base_of<basic_ostream, candidate>::value; 
}; 

class SomeType {}; 

template<typename Ostream> 
typename std::enable_if<is_ostream<Ostream>::value, Ostream&&>::type 
    operator<<(Ostream&& is, SomeType const& x) 
{ 
    is << "SomeType"; 
    return std::forward<Ostream>(is); 
} 

int main() 
{ 
    SomeType t; 

    std::string s = (std::ostringstream() << t).str(); 

    std::cout << s << std::endl; 
} 

De hecho, compila con gcc (usando la opción -std=c++0x), y cuando se ejecuta salidas SomeType como se esperaba. Sin embargo, es una maquinaria bastante complicada llegar allí.

Por tanto, mi pregunta:

  1. es el operador de salida como lo escribí de hecho funciona como se esperaba (y cumpliendo todos los requisitos que le di)? ¿O podría fallar de formas inesperadas o tener otras consecuencias inesperadas? Especialmente: ¿Es correcto el uso de std::forward aquí?

  2. ¿Existe alguna manera más fácil de cumplir los requisitos?

  3. Asumiendo que esto es realmente correcto y la forma más simple de hacerlo: ¿Considera que es una buena idea hacer eso, o le aconsejaría en contra (específicamente para la salida de flujo como en mi ejemplo, y como un esquema general para pasar objetos a través de funciones)?

+0

"' std :: ostringstream() 'es un valor r, pero' operator << 'toma un lvalue como primer argumento" - en C++ 11, hay una sobrecarga para rvalues ​​('ostream & operator << (ostream && o, T const &) 'plantilla), que simplemente reenvía a la versión lvalue. Además, este problema "rvalue" solo existía en C++ 03 para aquellas sobrecargas 'operator <<' que se definieron fuera de clase. Además, 'operator <<' devuelve un 'ostream', lo edité por ti. Por último, sí, las transmisiones C++ 11 son realmente móviles. – Xeo

+0

@Xeo: Gracias por la información. Sin embargo, la sobrecarga rvalue todavía no cumple con todos mis criterios: todavía devuelve un 'istream' en lugar de la clase exacta (es decir, todavía no podría llamar' str() 'al final), y (asumiendo no te olvidaste de un '&') sino que también elimina la validación (violando mi tercera condición). – celtschk

+0

Sigues escribiendo 'istream' en lugar de' ostream' ... :) Y no, no me olvidé de un '&', de hecho descarta la validez. Solo quería mencionar que tal sobrecarga existe, ya que invalida uno de sus puntos problemáticos. – Xeo

Respuesta

6

std::ostringstream() es un valor de lado derecho, pero operator<< toma un valor izquierdo como primer argumento

Hay un insertor genérica para tomar corrientes rvalue, pero devuelve un basic_ostream<charT, traits>&:

template <class charT, class traits, class T> 
    basic_ostream<charT, traits>& 
    operator<<(basic_ostream<charT, traits>&& os, const T& x); 

Para que funcione correctamente para su ejemplo, debe devolver el tipo derivado de la secuencia (std::ostringstram).

  1. ¿Es el operador de salida como lo escribí de hecho funciona como se esperaba (y cumpliendo todos los requisitos que le di)? ¿O podría fallar en maneras inesperadas o tener otras consecuencias inesperadas? Especialmente: ¿Es mi uso de std::forward correcto aquí?

Tu código me parece correcto.

  1. ¿Hay una manera más fácil para cumplir con los requisitos?

Su código es similar al código que escribí para resolver este problema (véase más adelante).

  1. Suponiendo que esto es de hecho correcta y la forma más sencilla de hacerlo: No se tiene en cuenta que es una buena idea hacer eso, o le consejos contra ella (tanto de forma específica para la salida de corriente como en mi ejemplo, y como un esquema general más para pasar objetos a través de funciones)?

El lenguaje se ve bien para mí.

En libc++ Definí el enrutador ostream rvalue como el siguiente (como una extensión).Hice un intento de lograr que se estandarizó pero era tarde y el comité comprensible que no estaba de humor para más goleada:

template <class _Stream, class _Tp> 
inline 
typename enable_if 
< 
    !is_lvalue_reference<_Stream>::value && 
    is_base_of<ios_base, _Stream>::value, 
    _Stream&& 
>::type 
operator<<(_Stream&& __os, const _Tp& __x) 
{ 
    __os << __x; 
    return std::move(__os); 
} 

A diferencia de la suya, esto sólo se acepta corrientes rvalue. Pero al igual que el tuyo, devuelve el tipo derivado de concreto, y así funciona con tu ejemplo.

+4

Gracias por su respuesta. Por cierto, creo que hay un error con la interacción de tus dos decisiones en tu biblioteca: como tu versión rvalue de 'operator <<' devuelve un lvalue, el operador 'encadenado <<' usará la versión lvalue. Y como la versión lvalue de 'operator <<' no conserva el tipo exacto, creo que su biblioteca permitirá '(std :: ostringstream() << foo) .str()' pero * not * '(std :: ostringstream() << foo << bar) .str() '. Lo que consideraría una inusual inconsistencia. – celtschk

+0

Ese es un buen punto, gracias. –

+0

La [respuesta de Howard a esta pregunta similar] (http://stackoverflow.com/a/8829047/201725) menciona que se discutió como [LWG 1203] (http://www.open-std.org/jtc1/sc22 /wg21/docs/lwg-closed.html#1203), pero no se acepta en el estándar. –