2009-07-19 15 views
7

He estado escribiendo una versión binaria de iostreams. Básicamente le permite escribir archivos binarios, pero le da mucho control sobre el formato del archivo. Ejemplo de uso: (., donde el prefijo es u16le)Versión binaria de iostream

my_file << binary::u32le << my_int << binary::u16le << my_string; 

escribiría my_int como un entero sin signo de 32 bits, y mi_cadena como una cadena de longitud prefijada Para leer el archivo de nuevo, sería voltear las flechas. Funciona genial. Sin embargo, me tocó un bache en el diseño, y todavía estoy en la valla al respecto. Entonces, es hora de preguntar SO. (Hacemos un par de suposiciones, tales como bytes de 8 bits, complementos de complemento 2 y flotantes IEEE en este momento).

iostreams, debajo del capó, use streambufs. Realmente es un diseño fantástico: iostreams codifica la serialización de un 'int' en texto, y deja que el streambuf subyacente se encargue del resto. Por lo tanto, obtienes cout, fstreams, stringstreams, etc. Todos estos, los iostreams y los streambufs, están modelados, generalmente en char, pero a veces también como un wchar. Sin embargo, mis datos son un flujo de bytes, que mejor se representa por 'unsigned char'.

Mis primeros intentos fueron crear plantillas de las clases basadas en unsigned char. std::basic_string plantillas lo suficientemente bien, pero streambuf no. Me encontré con varios problemas con una clase llamada codecvt, que nunca pude seguir el tema unsigned char. Esto plantea dos preguntas:

1) ¿Por qué es un streambuf responsable de tales cosas? Parece que las conversiones de código se encuentran fuera de la responsabilidad de un streambuf: streambufs debería tomar una secuencia y almacenar datos en búfer desde/hacia ella. Nada mas. Algo de alto nivel como conversiones de código parece que debería pertenecer a iostreams.

Como no pude hacer que los streambufs con plantillas trabajaran con char sin signo, volví a char, y simplemente fundí datos entre char/unsigned char. Traté de minimizar el número de lanzamientos, por razones obvias. La mayoría de los datos básicamente terminan en una función de lectura() o de escritura(), que luego invoca el streambuf subyacente. (Y use un molde en el proceso.) La función de lectura es básicamente:

size_t read(unsigned char *buffer, size_t size) 
{ 
    size_t ret; 
    ret = stream()->sgetn(reinterpret_cast<char *>(buffer), size); 
    // deal with ret for return size, eof, errors, etc. 
    ... 
} 

¿Buena solución, mala solución?


Las dos primeras preguntas indican que se necesita más información. En primer lugar, se analizaron proyectos como boost :: serialization, pero existen en un nivel superior, ya que definen su propio formato binario. Esto es más para leer/escribir en un nivel inferior, donde se desea definir el formato, o el formato ya está definido, o los metadatos no son necesarios o deseados.

En segundo lugar, algunos han preguntado sobre el modificador binary::u32le. Es una ejemplificación de una clase que posee el anhelo y el ancho deseados, por el momento, tal vez firmado en el futuro. La secuencia contiene una copia de la última instancia pasada de esa clase, y la usó en la serialización. Este fue un poco de una solución, que originalmente intentó sobrecarga para el operador < < así:

bostream &operator << (uint8_t n); 
bostream &operator << (uint16_t n); 
bostream &operator << (uint32_t n); 
bostream &operator << (uint64_t n); 

Sin embargo en el momento, esto no parece funcionar. Tuve varios problemas con la llamada de función ambigua. Esto fue especialmente cierto en el caso de las constantes, aunque podría, como sugirió un afiche, emitir o simplemente declararlo como const <type>. Me parece recordar que había otro problema más grande sin embargo.

+0

Su problema de llamada de función ambigua puede deberse al hecho de que [u] int32_t se suele definir como "[unsigned] long". Por lo tanto, si intenta escribir un "[unsigned] int", que suelen ser los literales numéricos, se necesita una promoción, pero el tipo para promocionar es ambiguo (por ejemplo, largo versus doble). –

+0

¿Puede por favor publicar la salida del compilador en esos conflictos que tuvo con unsigned char streambuf? – Basilevs

Respuesta

1

Según tengo entendido, las propiedades de transmisión que está utilizando para especificar tipos serían más apropiadas para especificar endianidad, empaque u otros valores de "metadatos". El manejo de los tipos debe ser realizado por el compilador. Al menos, esa es la forma en que el STL parece estar diseñado.

Si utiliza sobrecargas para separar los tipos de forma automática, lo que tendría que especificar el tipo sólo cuando era diferente del tipo declarado de la variable:

Stream& operator<<(int8_t); 
Stream& operator<<(uint8_t); 
Stream& operator<<(int16_t); 
Stream& operator<<(uint16_t); 
etc. 

uint32_t x; 
stream << x << (uint16_t)x; 

lectura tipos distintos del tipo declarado sería un poco más desordenado En general, sin embargo, se debe evitar la lectura o la escritura de variables de un tipo diferente al tipo de salida, creo.

Creo que la versión predeterminada de std :: codecvt no hace nada, devolviendo "noconv" para todo. Solo hace algo al utilizar las transmisiones de caracteres "anchos". ¿No puedes configurar una definición similar para codecvt? Si, por alguna razón, no es práctico definir un codecv no operativo para tu transmisión, entonces no veo ningún problema con tu solución de conversión, especialmente porque está aislada en una ubicación.

Finalmente, ¿está seguro de que no sería mejor utilizar un código de serialización estándar, como Boost, en lugar de hacer el suyo propio?

+1

Boost :: la serialización reside en un nivel ligeramente más alto, y no se puede utilizar para ayudar a la lectura de protocolos binarios existentes, por ejemplo, o dar bastante el control de grano fino. En cuanto al codecvt, hice algunas puñaladas iniciales al escribir una no operativa para char sin signo, pero no tuvo éxito. En cuanto a las definiciones, originalmente comencé con lo que tenías, pero encontré problemas con las llamadas a funciones ambiguas y pasé a la solución actual. Podría volver a intentarlo, ya que usar el tipo sería mucho más natural. – Thanatos

+0

Bueno, parece que sabes lo que estás haciendo. Esa es toda la retroalimentación que tengo. Si quiere echar un vistazo a los problemas de sobrecarga o codecvt, estoy seguro de que SO estaría feliz de verlo. –

+0

Creo que este es el camino a seguir. Tal vez deberías intentar trabajar con los errores de 'llamada de función ambigua'. No creo (no lo he probado) que el diseño de sobrecarga sea defectuoso (puedo probar que estaba equivocado) –

0

Necesitamos hacer algo similar a lo que está haciendo, pero seguimos otro camino. Estoy interesado en cómo ha definido su interfaz. Parte de lo que no sé cómo puede manejar son los manipuladores que ha definido (binario :: u32le, binaryu16le).

Con basic_streams, el manipulador controla cómo se leerán/escribirán todos los elementos siguientes, pero en su caso, probablemente no tenga sentido, ya que el tamaño (parte de la información del manipulador) se ve afectado por la variable pasada y fuera.

binary_istream in; 
int i; 
int i2; 
short s; 
in >> binary::u16le >> i >> binary::u32le >> i2 >> s; 

En el código anterior, puede tener sentido de determinar que si la variable i es de 32 bits (suponiendo int es de 32 bits) que desea extraer de la corriente en serie sólo de 16 bits, mientras que desea extraer el 32 bits completos en i2. Después de eso, el usuario se ve obligado a introducir manipuladores para cada uno de los tipos que se pasan, o bien el manipulador todavía tiene efecto y cuando se pasa el cortocircuito y se leen 32 bits con un posible desbordamiento, y de cualquier manera el usuario probablemente obtendrá resultados inesperados.

El tamaño no parece pertenecer (en mi opinión) a los manipuladores.

Como nota al margen, en nuestro caso, ya que teníamos otras limitaciones como la definición del tiempo de ejecución de los tipos, y terminamos construyendo nuestro propio meta-tipo-sistema para construir tipos en tiempo de ejecución (un tipo de variante), y luego terminamos implementando de/serialization para esos tipos (estilo boost), por lo que nuestros serializadores no funcionan con tipos básicos de C++, sino con pares de serialización/datos.

+0

He hecho algunos cambios a la pregunta con respecto a su respuesta. Tú y el otro afiche ahora me han hecho replantear mi uso de manipuladores (buen nombre, necesitaron uno ...). Siento que hubo problemas con las llamadas a funciones ambiguas. Esto es especialmente true de en << 6, aunque esto se puede hacer con << uint16_t (6). Los manipuladores persisten y es un error intentar leer en una variable de 16 bits con un manipulador de 32 bits presente. Sin embargo, voy a pensar en este uso y ver si tal vez el patrón descrito por ustedes dos se ajusta mejor. – Thanatos

+0

El nombre no es mío, sino estándar. En el estándar C++, el capítulo 27.6 se titula: 'Formateado y manipuladores', y a diferencia de su versión, no se implementan como objetos que se pasan a la secuencia sino como funciones libres (plantilla) que se ejecutan dentro de la secuencia (ios_base o basic_ios <>). En cada caso toman y devuelven referencias al tipo dado (basic_stream <>, basic_ios <> o ios_base) –

0

No utilizaría el operador < < ya que está íntimamente asociado con E/S de texto formateado.

No utilizaría en absoluto una sobrecarga de operador para esto. Encontraría otro modismo.

+1

Considero su uso solo en el movimiento de cosas hacia/desde un flujo, pero quizás veo su punto. No obstante, es solo una llamada a función: podría reemplazarse fácilmente con .read (...) o .write (...), pero luego terminaría con: stream.read (x) .read (y) .read (z) que puede o no tener sentido. Sin embargo, dado que la jerarquía de clases coincide con la de iostreams, ¿por qué no también la API? – Thanatos

+0

La biblioteca estándar ya tiene métodos que pueden leer y escribir datos binarios. Son métodos, no sobrecargas de << or >>.La razón por la que sugiero no usar << and >> para la E/S binaria es que la gente piensa en la entrada y salida formateada cuando ven esos operadores. El formateo implica cosas como configuraciones regionales, justificación de campo, etc. No está haciendo nada de eso con la E/S binaria, razón por la cual la biblioteca estándar proporciona los métodos basic_istream :: read y basic_ostream :: write. – legalize

+0

Estás haciendo la mayor parte de eso con la E/S binaria. locale contiene endiannes o formatos de coma flotante, la justificación de los campos se reemplaza por la alineación. La salida formateada en binario es similar a la del texto uno. – Basilevs

2

Estoy de acuerdo con legalizar. Necesitaba hacer casi exactamente lo que está haciendo, y examiné la sobrecarga de <</>>, pero llegué a la conclusión de que iostream simplemente no estaba diseñado para acomodarlo.Por un lado, no quería tener que subclasificar las clases de flujo para poder definir mis sobrecargas.

Mi solución (que sólo es necesario para serializar datos de forma temporal en una sola máquina, y por lo tanto no era necesario abordar orden de bits) se basó en este patrón:

// deducible template argument read 
template <class T> 
void read_raw(std::istream& stream, T& value, 
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0) 
{ 
    stream.read(reinterpret_cast<char*>(&value), sizeof(value)); 
} 

// explicit template argument read 
template <class T> 
T read_raw(std::istream& stream) 
{ 
    T value; 
    read_raw(stream, value); 
    return value; 
} 

template <class T> 
void write_raw(std::ostream& stream, const T& value, 
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0) 
{ 
    stream.write(reinterpret_cast<const char*>(&value), sizeof(value)); 
} 

entonces sobrecargado aún más read_raw/write_raw para cualquier tipos no POD (por ejemplo, cadenas). Tenga en cuenta que solo la primera versión de read_raw debe estar sobrecargada; si use ADL correctly, la segunda versión (1-arg) puede invocar sobrecargas de 2-arg definidas más adelante y en otros espacios de nombres.

Escribir ejemplo:

int32_t x; 
int64_t y; 
int8_t z; 
write_raw(is, x); 
write_raw(is, y); 
write_raw<int16_t>(is, z); // explicitly write int8_t as int16_t 

Lee ejemplo:

int32_t x = read_raw<int32_t>(is); // explicit form 
int64_t y; 
read_raw(is, y); // implicit form 
int8_t z = numeric_cast<int8_t>(read_raw<int16_t>(is)); 

No es tan atractivo como operadores sobrecargados, y las cosas no cabe en una línea tan fácilmente (que tienden a evitar todos modos , ya que los puntos de corte de depuración están orientados a la línea), pero creo que resultó ser más simple, más obvio y no mucho más detallado.