c++ c++11 language-lawyer

c++ - reinterpretar_cast entre char*y std:: uint8_t*- ¿seguro?



c++11 language-lawyer (2)

Ahora, a veces, todos tenemos que trabajar con datos binarios. En C ++ trabajamos con secuencias de bytes, y desde el principio char era nuestro componente básico. Definido para tener sizeof de 1, es el byte. Y todas las funciones de E / S de la biblioteca usan char por defecto. Todo está bien, pero siempre hubo una pequeña preocupación, una pequeña rareza que molestó a algunas personas: el número de bits en un byte está definido por la implementación.

Entonces, en C99, se decidió introducir varios typedefs para que los desarrolladores puedan expresarse fácilmente, los tipos enteros de ancho fijo. Opcional, por supuesto, ya que nunca queremos dañar la portabilidad. Entre ellos, uint8_t , migrado a C ++ 11 como std::uint8_t , un tipo de entero sin signo de 8 bits de ancho fijo, era la opción perfecta para las personas que realmente querían trabajar con bytes de 8 bits.

Y así, los desarrolladores adoptaron las nuevas herramientas y comenzaron a construir bibliotecas que expresamente afirman que aceptan secuencias de bytes de 8 bits, como std::uint8_t* , std::vector<std::uint8_t> u otras.

Pero, quizás con una reflexión muy profunda, el comité de estandarización decidió no requerir la implementación de std::char_traits<std::uint8_t> por lo tanto, prohibió a los desarrolladores crear de manera fácil y portátil, por ejemplo, std::basic_fstream<std::uint8_t> y leyendo fácilmente std::uint8_t s como datos binarios. O tal vez, a algunos de nosotros no nos importa la cantidad de bits en un byte y estamos contentos con eso.

Desafortunadamente, dos mundos colisionan y, a veces, debes tomar una información como char* y pasarla a una biblioteca que espera std::uint8_t* . Pero espera, dices, ¿no es un bit variable char y std::uint8_t está fijado en 8? ¿Se traducirá en una pérdida de datos?

Bueno, hay un Standardese interesante sobre esto. El char definido para contener exactamente un byte y un byte es el fragmento de memoria más bajo direccionable, por lo que no puede haber un tipo con ancho de bit menor que el de char . A continuación, se define para poder mantener unidades de código UTF-8. Esto nos da el mínimo - 8 bits. Entonces ahora tenemos un typedef que requiere 8 bits de ancho y un tipo que tiene al menos 8 bits de ancho. Pero hay alternativas? Sí, unsigned char Recuerde que la firma de caracteres está definida por la implementación. ¿Algún otro tipo? Afortunadamente, no. Todos los otros tipos integrales tienen rangos requeridos que caen fuera de 8 bits.

Finalmente, std::uint8_t es opcional, eso significa que la biblioteca que usa este tipo no compilará si no está definido. Pero, ¿y si compila? Puedo decir con un alto grado de confianza que esto significa que estamos en una plataforma con bytes de 8 bits y CHAR_BIT == 8 .

Una vez que tenemos este conocimiento, que tenemos bytes de 8 bits, que std::uint8_t se implementa como char o unsigned char , ¿podemos suponer que podemos hacer reinterpret_cast desde char* a std::uint8_t* y viceversa? ¿Es portátil?

Aquí es donde mis habilidades de lectura de Standardese me fallan. Leí sobre punteros derivados de forma segura ( [basic.stc.dynamic.safety] ) y, por lo que yo entiendo, lo siguiente:

std::uint8_t* buffer = /* ... */ ; char* buffer2 = reinterpret_cast<char*>(buffer); std::uint8_t buffer3 = reinterpret_cast<std::uint8_t*>(buffer2);

es seguro si no tocamos buffer2 . Corrígeme si estoy equivocado.

Entonces, dadas las siguientes precondiciones:

  • CHAR_BIT == 8
  • std::uint8_t está definido.

¿Es portátil y seguro emitir char* y std::uint8_t* hacia adelante y hacia atrás, suponiendo que estamos trabajando con datos binarios y que la posible falta de signo de char no importa?

Agradecería las referencias al estándar con explicaciones.

EDITAR: Gracias, Jerry Coffin. Voy a agregar la cita del Estándar ([basic.lval], §3.10 / 10):

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido:

...

- un tipo de carácter char o unsigned.

EDIT2: Ok, profundizando. std::uint8_t no garantiza que sea un typedef de unsigned char . Se puede implementar como tipo de entero extendido sin signo y los tipos de entero extendido sin signo no se incluyen en §3.10 / 10. ¿Ahora que?


Ok, seamos verdaderamente pedantes. Después de leer this , this y this , estoy bastante seguro de que entiendo la intención detrás de ambos Estándares.

Por lo tanto, hacer reinterpret_cast desde std::uint8_t* a char* y luego eliminar la referencia del puntero resultante es seguro y portátil, y está explícitamente permitido por [basic.lval] .

Sin embargo, hacer reinterpret_cast desde char* a std::uint8_t* y luego eliminar la referencia del puntero resultante es una violación de la regla de aliasing estricta y es un comportamiento indefinido si std::uint8_t se implementa como tipo de entero extendido sin signo .

Sin embargo, hay dos posibles soluciones, primero:

static_assert(std::is_same_v<std::uint8_t, char> || std::is_same_v<std::uint8_t, unsigned char>, "This library requires std::uint8_t to be implemented as char or unsigned char.");

Con esta afirmación en su lugar, su código no se compilará en plataformas en las que daría como resultado un comportamiento indefinido en caso contrario.

Segundo:

std::memcpy(uint8buffer, charbuffer, size);

Cppreference dice que std::memcpy accede a los objetos como matrices de caracteres unsigned char por lo que es seguro y portátil .

Para reiterar, para poder reinterpret_cast entre char* y std::uint8_t* y trabajar con los punteros resultantes de forma portátil y segura en una forma 100% estándar, las siguientes condiciones deben ser verdaderas:

  • CHAR_BIT == 8 .
  • std::uint8_t está definido.
  • std::uint8_t se implementa como char o unsigned char .

En una nota práctica, las condiciones anteriores son ciertas en el 99% de las plataformas y es probable que no haya ninguna plataforma en la que las dos primeras condiciones sean verdaderas, mientras que la tercera sea falsa.


Si uint8_t existe, esencialmente la única opción es que es un typedef para unsigned char (o char si no está firmado). Nada (excepto un campo de bits) puede representar menos almacenamiento que un char , y el único otro tipo que puede ser tan pequeño como 8 bits es un bool . El siguiente tipo entero normal más pequeño es un short , que debe ser de al menos 16 bits.

Como tal, si uint8_t existe, solo tienes dos posibilidades: estás lanzando unsigned char unsigned char , o lanzando signed char unsigned char .

El primero es una conversión de identidad, tan obviamente seguro. Este último cae dentro de la "dispensación especial" dada para acceder a cualquier otro tipo como una secuencia de char o char sin signo en §3.10 / 10, por lo que también proporciona un comportamiento definido.

Como eso incluye caracteres char y unsigned char , un molde para acceder a él como una secuencia de caracteres también proporciona un comportamiento definido.

Editar: En lo que se refiere a la mención de tipos de enteros extendidos de Luc, no estoy seguro de cómo se las arreglaría para aplicarla para obtener una diferencia en este caso. C ++ se refiere al estándar C99 para las definiciones de uint8_t y tal, por lo que las citas en el resto de esto provienen de C99.

El §6.2.6.1 / 3 especifica que el unsigned char debe usar una representación binaria pura, sin bits de relleno. Los bits de relleno solo están permitidos en 6.2.6.2/1, lo que excluye específicamente el unsigned char . Esa sección, sin embargo, describe una representación binaria pura en detalle, literalmente al bit. Por lo tanto, unsigned char y uint8_t (si existe) se deben representar de forma idéntica en el nivel de bit.

Para ver una diferencia entre los dos, debemos afirmar que algunos bits particulares, cuando se ven como uno, producirían resultados diferentes de los que se percibirían como el otro, a pesar de que los dos deben tener representaciones idénticas en el nivel de bit.

Para decirlo más directamente: una diferencia en el resultado entre los dos requiere que interpreten los bits de manera diferente, a pesar del requisito directo de que interpreten los bits de manera idéntica.

Incluso en un nivel puramente teórico, esto parece difícil de lograr. En cualquier cosa que se aproxime a un nivel práctico, es obviamente ridículo.