Se permite aliasing T* con char*. ¿También está permitido al revés?


Nota: Esta pregunta ha sido renombrada y reducida para hacerla más enfocada y legible. La mayoría de los comentarios se refieren al texto antiguo.


De acuerdo con el estándar, los objetos de tipo diferente pueden no compartir la misma ubicación de memoria. Así que 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 a través de un puntero a char o unsigned char:

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

Sin embargo, no me queda claro si esto también es permitido al revés. Por ejemplo:

char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?
Author: alfC, 2012-09-27

3 answers

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 a través de cv void* cuando se trate de almacenamiento.)

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

El 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 estrictos', de 3.10 Lvalues y rvalues [basic.lval]:

10 Si un programa intenta acceder al valor almacenado de un objeto a través de glvalue de otro de los siguientes tipos el comportamiento es indefinido:

  • el tipo dinámico del objeto,
  • una versión calificada cv 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 sin firmar correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar que corresponde a una versión 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 miembro de datos no estáticos de una subagregada o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • un tipo char o char 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 con, por ejemplo, 'lvalue' que se utiliza 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 este sería reinterpret_cast, lo que produce resultados no especificados en algunos casos, pero como he explicado antes, esto no es así para los punteros a tipos de diseño estándar. Entonces es verdad que todos de sus fragmentos son correctos (sustituyendo reinterpret_cast por magic_cast), porque no hay glvalues involucrados en absoluto con los resultados de magic_cast.

Aquí hay un fragmento que aparece para 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, asume este fragmento superficialmente diferente: {[47]]}

// 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 a pesar de que c fue destruido debido a la reglas establecidas en el párrafo 5 de 3.8 Duración del objeto [básico.vida]. El párrafo 6 de same da reglas muy similares a las referencias al almacenamiento, y el párrafo 7 explica lo que sucede con las variables, punteros y referencias que solían referirse a un objeto una vez que su almacenamiento se reutiliza.Me referiré colectivamente a los como 3.8/5-7.

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

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

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{[59]]}
  • si el objeto tiene inicialización no trivial, su inicialización es completa.

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

  • si T es un tipo de clase con un destructor no trivial (12.4), se inicia la llamada a 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.

Ten en cuenta que p no se puede plegar. Es decir, los siguientes es definitivamente incorrecto:

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

Si asumimos que un objeto int está (trivialmente) construido con la segunda línea, entonces eso debe significar &c se convierte en un puntero al almacenamiento que ha sido reutilizado. Por lo tanto, la tercera línea es incorrecta although 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)] a través de un glvalue de tipo int, que no es una de las excepciones permitidas. En comparación, *magic_cast<unsigned char>(&c) = 42; estaría bien (asumiríamos que un objeto short está construido trivialmente en la tercera línea).

Al igual que Alf, también recomendaría que haga uso explícito de la colocación Estándar new cuando use almacenamiento. Saltar la destrucción para los tipos triviales está bien, pero cuando se encuentra *some_magic_pointer = foo; es muy probable que se enfrente a una violación de 3.8 / 5-7 (no importa cuán mágicamente se obtuvo ese puntero) o de la reglas de aliasing. Esto significa almacenar el resultado de la nueva expresión, también, ya que lo más probable es que no pueda reutilizar el puntero mágico una vez que se construye su objeto due debido a 3.8/5-7 de nuevo.

Leer los bytes de un objeto (esto significa usar char o unsigned char) está bien, sin embargo, y ni siquiera debes usar reinterpret_cast ni nada mágico. static_cast a través de cv void* podría decirse que está bien para el trabajo (aunque siento que el Estándar podría usar algunas palabras mejores allí).

 22
Author: Luc Danton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-27 07:02:51

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 establecen en qué circunstancias es legal / ilegal acceder a un objeto a través de un lvalue de un tipo diferente. Hay una regla específica que dice que se puede acceder a cualquier objeto a través de 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 un int a través de un puntero a char, pero no puede acceder a un char a través de 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 que no sea uno de los siguientes tipos, el comportamiento es indefinido:

  • el tipo dinámico del objeto,
  • una versión calificada cv 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 sin firmar correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada cv 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 miembro de datos no estáticos de una subagregada o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico de el objeto,
  • un tipo char o char sin signo.
 6
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-27 03:04:22

Con respecto a la validez de {

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

El reinterpret_cast en sí está bien o no, en el sentido de producir un valor de puntero útil, dependiendo del compilador. Y en este ejemplo no se usa el resultado, en particular, no se accede a la matriz de caracteres. Así que no hay mucho más que se pueda 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 solo consideremos el caso cuando el compilador garantiza un valor de puntero útil, uno que colocaría el puntero 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 conversiones de puntero donde los tipos son compatibles con la alineación, y de lo contrario lo dejan como "no especificado" (pero entonces, el conjunto de §5.2.10 es algo inconsistente con §9.2/18).

Ahora, una interpretación de la norma §3.10 / 10, la llamada " estricta cláusula aliasing " (pero tenga en cuenta que el estándar nunca usa el término "aliasing estricto"),

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue que no sea uno de los siguientes tipos, el comportamiento es indefinido:

  • el tipo dinámico del objeto,
  • una versión calificada cv del tipo dinámico del objeto,
  • un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
  • un tipo es decir, el tipo firmado o sin firmar correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada cv 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 miembro de datos no estáticos de una subagregada o unión contenida),
  • un tipo que es a (posiblemente tipo de clase base del tipo dinámico del objeto,
  • a char o unsigned char tipo.

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

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. Así que en este caso, se accede a una matriz char a través de un puntero int*. Y nadie tiene ninguna duda de que el otro way es válido: aunque foo puede haber colocado un objeto inten esos bytes, puede acceder libremente a ese objeto como una secuencia de valores char, por el último guión de §3.10/10.

Así que con esta interpretación (habitual), después de que foo ha colocado un int allí, podemos acceder a él como char objetos, por lo 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 allí también; y así la afirmación de David en otra respuesta que char no se puede acceder a los objetos como int, es incompatible con esta interpretación habitual.

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

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

Así que en conclusión, en lo que respecta a la Norma Sagrada, simplemente lanzarse un T* puntero to the array es prácticamente útil o no dependiendo del compilador, y acceder al valor apuntado a podría ser válido o no dependiendo de lo que esté presente. En particular, piense en una representación trampa de int: no querría que explotara en usted, si el patrón de bits fuera eso. Así que para estar seguro tienes que saber lo que hay ahí, los bits, y como la llamada a foo anterior ilustra el compilador en general no puede saber que, como, el compilador de g++ es estricto optimizador basado en alineación puede en general no saber que {

 2
Author: Cheers and hth. - Alf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:07