c++ language-lawyer strict-aliasing type-punning

c++ - Aliasing T*con char*está permitido. ¿También se permite al revés?



language-lawyer strict-aliasing (3)

Nota: Esta pregunta se ha cambiado de nombre y se ha reducido para que sea más enfocada y legible. La mayoría de los comentarios se refieren al texto anterior.

De acuerdo con el estándar, los objetos de diferente tipo pueden no compartir la misma ubicación de memoria. Entonces esto no sería legal:

std::array<short, 4> shorts; int* i = reinterpret_cast<int*>(shorts.data()); // Not OK

El estándar, sin embargo, permite una excepción a esta regla: se puede acceder a cualquier objeto mediante un puntero a char o unsigned char :

int i = 0; char * c = reinterpret_cast<char*>(&i); // OK

Sin embargo, no está claro si esto también se permite al revés. Por ejemplo:

char * c = read_socket(...); unsigned * u = reinterpret_cast<unsigned*>(c); // huh?


Esto también:

// valid: char -> type alignas(int) char c[sizeof(int)]; int * i = reinterpret_cast<int*>(c);

Eso no es correcto. Las reglas de alias definen bajo qué circunstancias es legal / ilegal acceder a un objeto a través de un valor l de un tipo diferente. Hay una regla específica que dice que puede acceder a cualquier objeto mediante un puntero de tipo char o unsigned char , por lo que el primer caso es correcto. Es decir, A => B no significa necesariamente B => A. Puede acceder a int través de un puntero a char , pero no puede acceder a un char mediante un puntero a int .

Para el beneficio de Alf:

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:

  • el tipo dinámico del objeto,
  • una versión cv-calificada del tipo dinámico del objeto,
  • un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
  • un agregado o tipo de unión que incluye uno de los tipos mencionados entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o un elemento de datos no estático de un subaggregado o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
  • un char o un tipo de carácter sin signo.

En cuanto a la validez de ...

alignas(int) char c[sizeof(int)]; int * i = reinterpret_cast<int*>(c);

El reinterpret_cast sí es correcto o no, en el sentido de producir un valor de puntero útil, según el compilador. Y en este ejemplo, el resultado no se utiliza, en particular, no se accede a la matriz de caracteres. Por lo tanto, no hay mucho más que decir sobre el ejemplo tal como está: simplemente depende .

Pero consideremos una versión extendida que toca las reglas de aliasing:

void foo( char* ); alignas(int) char c[sizeof( int )]; foo( c ); int* p = reinterpret_cast<int*>( c ); cout << *p << endl;

Y consideremos solo el caso donde el compilador garantiza un valor de puntero útil, uno que colocaría el punto en los mismos bytes de memoria (la razón por la que esto depende del compilador es que el estándar, en §5.2.10 / 7, solo lo garantiza para las conversiones de puntero donde los tipos son compatibles con la alineación, y de lo contrario lo deja como "no especificado" (pero entonces, la totalidad de §5.2.10 es algo inconsistente con §9.2 / 18).

Ahora, una interpretación de la norma §3.10 / 10, la llamada cláusula de "alias estricto" (pero tenga en cuenta que la norma no utiliza el término "aliasing estricto"),

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:

  • el tipo dinámico del objeto,
  • una versión cv-calificada del tipo dinámico del objeto,
  • un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
  • un agregado o tipo de unión que incluye uno de los tipos mencionados entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o un elemento de datos no estático de un subaggregado o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
  • un char o un tipo de char unsigned .

es que, como dice él mismo, se refiere al tipo dinámico del objeto que reside en los bytes c .

Con esa interpretación, la operación de lectura en *p está bien si foo ha colocado un objeto int allí, y de lo contrario no. Entonces, en este caso, se accede a una matriz char a través de un puntero int* . Y nadie duda de que la otra vía es válida: aunque foo puede haber colocado un objeto int en esos bytes, puede acceder libremente a ese objeto como una secuencia de valores char , en el último guión de §3.10 / 10.

Entonces con esta interpretación (usual), después de que foo haya colocado un int allí, podemos acceder a él como objetos char , de modo que al menos un objeto char existe dentro de la región de memoria llamada c ; y podemos acceder a él como int , por lo que al menos ese int existe también allí; y por lo tanto, la afirmación de David en otra respuesta de que los objetos char no pueden ser accedidos como int , es incompatible con esta interpretación usual.

La afirmación de David también es incompatible con el uso más común de la colocación nueva.

Con respecto a otras interpretaciones posibles, que tal vez podrían ser compatibles con la afirmación de David, bueno, no puedo pensar en ninguna que tenga sentido.

Por lo tanto, en conclusión, en lo que se refiere a la norma sagrada, simplemente lanzarse un puntero T* a la matriz es prácticamente útil o no según el compilador, y el acceso a la posible validez es válido o no según lo que presente. En particular, piense en una representación de trampa de int : no le gustaría que explotara sobre usted, si el patrón de bits fuera eso. Así que para estar seguro debes saber qué hay allí, los bits, y como la llamada a foo arriba ilustra, el compilador en general puede no saber que , como el optimizador basado en la alineación estricto del compilador g ++, en general, no puede saber ...


Parte de su código es cuestionable debido a las conversiones de puntero involucradas. Tenga en cuenta que en esos casos reinterpret_cast<T*>(e) tiene la semántica de static_cast<T*>(static_cast<void*>(e)) porque los tipos que están involucrados son de diseño estándar. (De hecho, recomendaría que siempre use static_cast través de cv void* cuando se trata de almacenamiento).

Una lectura detallada del estándar sugiere que durante la conversión de un puntero hacia o desde T* se supone que realmente hay un objeto real T* involucrado, lo cual es difícil de cumplir en algunos de sus fragmentos, incluso cuando se ''engaña'' gracias a la trivialidad de los tipos involucrados (más sobre esto más adelante). Eso sería además el punto sin embargo porque ...

Aliasing no se trata de conversiones de puntero. Este es el texto de C ++ 11 que describe las reglas que comúnmente se conocen como reglas de ''alias estricto'', a partir de 3.10 valores L y valores [basic.lval]:

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

  • el tipo dinámico del objeto,
  • una versión cv-calificada del tipo dinámico del objeto,
  • un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o un elemento de datos no estático de un subaggregado o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
  • un char o un tipo de carácter sin signo.

(Este es el párrafo 15 de la misma cláusula y subcláusula en C ++ 03, con algunos cambios menores en el texto, por ejemplo, se usa ''lvalue'' en lugar de ''glvalue'' ya que este último es una noción de C ++ 11).

A la luz de esas reglas, supongamos que una implementación nos proporciona magic_cast<T*>(p) que ''de alguna manera'' convierte un puntero a otro tipo de puntero. Normalmente esto sería reinterpret_cast , que produce resultados no especificados en algunos casos, pero como he explicado antes, esto no es así para los punteros a los tipos de diseño estándar. Entonces, es completamente cierto que todos los fragmentos son correctos (sustituyendo reinterpret_cast con magic_cast ), porque no hay glvalues ​​involucrados con los resultados de magic_cast .

Aquí hay un fragmento que parece usar incorrectamente magic_cast , pero que argumentaré que es correcto:

// assume constexpr max constexpr auto alignment = max(alignof(int), alignof(short)); alignas(alignment) char c[sizeof(int)]; // I''m assuming here that the OP really meant to use &c and not c // this is, however, inconsequential auto p = magic_cast<int*>(&c); *p = 42; *magic_cast<short*>(p) = 42;

Para justificar mi razonamiento, asuma este fragmento superficialmente diferente:

// alignment same as before alignas(alignment) char c[sizeof(int)]; auto p = magic_cast<int*>(&c); // end lifetime of c c.~decltype(c)(); // reuse storage to construct new int object new (&c) int; *p = 42; auto q = magic_cast<short*>(p); // end lifetime of int object p->~decltype(0)(); // reuse storage again new (p) short; *q = 42;

Este fragmento está cuidadosamente construido. En particular, en new (&c) int; Se me permite usar &c aunque c se destruyó debido a las reglas establecidas en el párrafo 5 de 3.8 Vida del objeto [basic.life]. El párrafo 6 de la misma proporciona reglas muy similares a las referencias al almacenamiento, y el párrafo 7 explica qué sucede con las variables, punteros y referencias que solían referirse a un objeto una vez que se reutiliza su almacenamiento: me referiré colectivamente a esos como 3.8 / 5- 7.

En este caso &c se convierte (implícitamente) en void* , que es uno de los usos correctos de un puntero al almacenamiento que aún no se ha reutilizado. De manera similar, p se obtiene de &c antes de que se construya la nueva int . Su definición podría quizás moverse a después de la destrucción de c , dependiendo de cuán profunda es la magia de implementación, pero ciertamente no después de la construcción int : se aplicaría el párrafo 7 y esta no es una de las situaciones permitidas. La construcción del objeto short también se basa en que p convierta en un indicador de almacenamiento.

Ahora, como int y short son tipos triviales, no tengo que usar las llamadas explícitas a los destructores. Tampoco necesito las llamadas explícitas a los constructores (es decir, las llamadas a la habitual, la colocación estándar nueva declarada en <new> ). Desde 3.8 Vida del objeto [basic.life]:

1 [...] La vida útil de un objeto de tipo T comienza cuando:

  • se obtiene el almacenamiento con la alineación y el tamaño adecuados para el tipo T, y
  • si el objeto tiene una inicialización no trivial, su inicialización está completa.

La vida útil de un objeto de tipo T finaliza cuando:

  • si T es un tipo de clase con un destructor no trivial (12.4), se inicia la llamada al destructor, o
  • el almacenamiento que ocupa el objeto se reutiliza o libera.

Esto significa que puedo reescribir el código de manera que, después de plegar la variable intermedia q , termine con el fragmento original.

Tenga en cuenta que p no se puede plegar. Es decir, lo siguiente es defintivamente incorrecto:

alignas(alignment) char c[sizeof(int)]; *magic_cast<int*>(&c) = 42; *magic_cast<short*>(&c) = 42;

Si suponemos que un objeto int se construye (trivialmente) con la segunda línea, eso debe significar que &c convierte en un puntero al almacenamiento que se ha reutilizado. Por lo tanto, la tercera línea es incorrecta, aunque debido a 3.8 / 5-7 y no debido a las reglas de aliasing estrictamente hablando.

Si no asumimos eso, entonces la segunda línea es una violación de las reglas de aliasing: estamos leyendo lo que en realidad es un objeto char c[sizeof(int)] través de un glvalue de tipo int , que no es uno de los permitidos excepción. En comparación, *magic_cast<unsigned char>(&c) = 42; estaría bien (supondríamos que un objeto short se construye trivialmente en la tercera línea).

Al igual que Alf, también recomendaría que utilice explícitamente la ubicación estándar nueva cuando utilice el almacenamiento. Saltarse la destrucción por tipos triviales está bien, pero al encontrar *some_magic_pointer = foo; es muy probable que enfrente una infracción de 3.8 / 5-7 (no importa cuán mágicamente se haya obtenido ese puntero) o de las reglas de aliasing. Esto significa almacenar el resultado de la nueva expresión, también, ya que lo más probable es que no puedas reutilizar el puntero mágico una vez que tu objeto esté construido, debido a 3.8 / 5-7 nuevamente.

Sin embargo, leer los bytes de un objeto (esto significa usar char o unsigned char ) está bien, y ni siquiera se puede usar reinterpret_cast o cualquier cosa mágica. static_cast via cv void* está posiblemente bien para el trabajo (aunque creo que el Standard podría usar una mejor redacción allí).