¿Cuál es la estricta regla de alias?


805

Al preguntar sobre el comportamiento indefinido común en C , las personas a veces se refieren a la estricta regla de alias.
De qué están hablando?


12
@Ben Voigt: las reglas de alias son diferentes para c ++ y c. ¿Por qué esta pregunta está etiquetada con cy c++faq.
MikeMB

66
@MikeMB: Si revisas el historial, verás que mantuve las etiquetas como estaban originalmente, a pesar del intento de algunos otros expertos de cambiar la pregunta de las respuestas existentes. Además, la dependencia del idioma y la dependencia de la versión es una parte muy importante de la respuesta a "¿Cuál es la estricta regla de alias?" y conocer las diferencias es importante para los equipos que migran código entre C y C ++, o escriben macros para usar en ambos.
Ben Voigt

66
@Ben Voigt: En realidad, por lo que puedo decir, la mayoría de las respuestas solo se relacionan con c y no con c ++, también la redacción de la pregunta indica un enfoque en las reglas C (o el OP simplemente no era consciente de que hay una diferencia ) En su mayor parte, las reglas y la idea general son las mismas, por supuesto, pero especialmente, en lo que respecta a los sindicatos, las respuestas no se aplican a c ++. Estoy un poco preocupado, porque algunos programadores de c ++ buscarán la estricta regla de alias y simplemente asumirán que todo lo que se indica aquí también se aplica a c ++.
MikeMB

Por otro lado, estoy de acuerdo en que es problemático cambiar la pregunta después de que se hayan publicado muchas buenas respuestas y el problema sea menor de todos modos.
MikeMB

1
@MikeMB: Creo que verá que la C se centra en la respuesta aceptada, por lo que es incorrecta para C ++, fue editada por un tercero. Esa parte probablemente debería revisarse nuevamente.
Ben Voigt

Respuestas:


562

Una situación típica en la que encuentra problemas de alias estrictos es cuando superpone una estructura (como un mensaje de dispositivo / red) en un búfer del tamaño de palabra de su sistema (como un puntero a uint32_tso uint16_ts). Cuando superpone una estructura en dicho búfer, o un búfer en dicha estructura a través de la conversión del puntero, puede violar fácilmente las estrictas reglas de alias.

Entonces, en este tipo de configuración, si quiero enviar un mensaje a algo, tendría que tener dos punteros incompatibles apuntando a la misma porción de memoria. Entonces podría codificar ingenuamente algo como esto (en un sistema con sizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

La estricta regla de alias hace que esta configuración sea ilegal: desreferenciar un puntero que alias un objeto que no es de un tipo compatible o uno de los otros tipos permitidos por C 2011 6.5 párrafo 7 1 es un comportamiento indefinido. Desafortunadamente, todavía puede codificar de esta manera, tal vez obtener algunas advertencias, compilarlo bien, solo para tener un comportamiento extraño e inesperado cuando ejecuta el código.

(GCC parece algo inconsistente en su capacidad de dar advertencias de aliasing, a veces dándonos una advertencia amistosa y otras no).

Para ver por qué este comportamiento es indefinido, tenemos que pensar en lo que la regla de alias estricto compra al compilador. Básicamente, con esta regla, no tiene que pensar en insertar instrucciones para actualizar el contenido de buffcada ejecución del ciclo. En cambio, cuando se optimiza, con algunas suposiciones molestas sobre el alias, puede omitir esas instrucciones, cargar buff[0]y buff[1] en los registros de la CPU una vez antes de que se ejecute el bucle, y acelerar el cuerpo del bucle. Antes de que se introdujera un alias estricto, el compilador tenía que vivir en un estado de paranoia que el contenido debuff podía cambiar en cualquier momento y en cualquier lugar. Entonces, para obtener una ventaja de rendimiento adicional, y suponiendo que la mayoría de las personas no escriben punteros, se introdujo la estricta regla de alias.

Tenga en cuenta que si cree que el ejemplo está ideado, esto podría suceder incluso si pasa un búfer a otra función que realiza el envío por usted, si es que lo ha hecho.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Y reescribió nuestro bucle anterior para aprovechar esta conveniente función

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

El compilador puede o no ser capaz o lo suficientemente inteligente como para intentar enviar SendMessage en línea y puede o no decidir cargar o no cargar buff nuevamente. Si SendMessagees parte de otra API que se compila por separado, probablemente tenga instrucciones para cargar el contenido de buff. Por otra parte, tal vez estás en C ++ y esta es una implementación de solo encabezado con plantilla que el compilador cree que puede en línea. O tal vez es algo que escribió en su archivo .c para su propia conveniencia. De todos modos, todavía podría seguir un comportamiento indefinido. Incluso cuando sabemos algo de lo que sucede debajo del capó, sigue siendo una violación de la regla, por lo que no se garantiza un comportamiento bien definido. Entonces, simplemente envolviendo una función que toma nuestro búfer delimitado por palabras no necesariamente ayuda.

Entonces, ¿cómo puedo evitar esto?

  • Usa una unión. La mayoría de los compiladores admiten esto sin quejarse de un alias estricto. Esto está permitido en C99 y explícitamente en C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Puede desactivar el alias estricto en su compilador ( f [no-] alias estricto en gcc))

  • Puede usar char*para crear alias en lugar de la palabra de su sistema. Las reglas permiten una excepción para char*(incluyendo signed chary unsigned char). Siempre se supone que char*alias otros tipos. Sin embargo, esto no funcionará de otra manera: no se supone que su estructura alias un búfer de caracteres.

Principiante ten cuidado

Este es solo un campo minado potencial cuando se superponen dos tipos entre sí. También debe aprender sobre endianness , alineación de palabras y cómo lidiar con problemas de alineación a través de estructuras de empaque correctamente.

Nota

1 Los tipos a los que C 2011 6.5 7 le permite acceder a un valor son:

  • un tipo compatible con el tipo efectivo del objeto,
  • una versión calificada de un tipo compatible con el tipo efectivo del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada del tipo efectivo del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión agregada o contenida), o
  • un tipo de personaje

16
Parece que voy después de la batalla ... ¿se unsigned char*puede usar lejos char*? Tiendo a usar en unsigned charlugar de charcomo el tipo subyacente byteporque mis bytes no están firmados y no quiero que la rareza del comportamiento firmado (especialmente wrt se desborde)
Matthieu M.

30
@Matthieu: la firma no hace ninguna diferencia para las reglas de alias, por lo que usar unsigned char *está bien.
Thomas Eding

22
¿No es un comportamiento indefinido leer de un miembro del sindicato diferente al último escrito?
R. Martinho Fernandes

23
Bollocks, esta respuesta es completamente al revés . El ejemplo que muestra como ilegal es realmente legal, y el ejemplo que muestra como legal es realmente ilegal.
R. Martinho Fernandes

77
Tu uint32_t* buff = malloc(sizeof(Msg));y las siguientes unsigned int asBuffer[sizeof(Msg)];declaraciones de búfer de unión tendrán diferentes tamaños y ninguna de ellas es correcta. La mallocllamada se basa en la alineación de 4 bytes debajo del capó (no lo hagas) y la unión será 4 veces más grande de lo necesario ... Entiendo que es por claridad, pero no me molesta. menos ...
nonsensickle

233

La mejor explicación que he encontrado es Mike Acton, Understanding Strict Aliasing . Se centra un poco en el desarrollo de PS3, pero eso es básicamente solo GCC.

Del artículo:

"El alias estricto es una suposición, hecha por el compilador de C (o C ++), de que desreferenciar los punteros a objetos de diferentes tipos nunca se referirá a la misma ubicación de memoria (es decir, alias entre sí)".

Así que, básicamente, si tiene una int*señal que apunta a alguna memoria que contiene una inty luego señala una float*a esa memoria y la usa como una floatinfracción de la regla. Si su código no respeta esto, entonces el optimizador del compilador probablemente romperá su código.

La excepción a la regla es un char* , que puede apuntar a cualquier tipo.


66
Entonces, ¿cuál es la forma canónica de usar legalmente la misma memoria con variables de 2 tipos diferentes? o todos simplemente copian?
jiggunjer

44
La página de Mike Acton es defectuosa. La parte de "Transmitir a través de una unión (2)", al menos, es totalmente errónea; el código que dice es legal no lo es.
davmac

11
@davmac: Los autores de C89 nunca tuvieron la intención de obligar a los programadores a saltar por los aros. Encuentro completamente extraño la noción de que una regla que existe con el único propósito de optimización debe interpretarse de tal manera que requiera que los programadores escriban código que copie datos de forma redundante con la esperanza de que un optimizador elimine el código redundante.
supercat

1
@curiousguy: "¿No se pueden tener sindicatos"? En primer lugar, el propósito original / primario de los sindicatos no está relacionado de ninguna manera con el alias. En segundo lugar, la especificación del lenguaje moderno permite explícitamente el uso de uniones para el alias. El compilador debe notar que se utiliza una unión y tratar la situación de una manera especial.
An

55
@ curiousguy: falso. En primer lugar, la idea conceptual original detrás de los sindicatos era que en cualquier momento solo hay un objeto miembro "activo" en el objeto de unión dado, mientras que los otros simplemente no existen. Por lo tanto, no hay "objetos diferentes en la misma dirección" como parece creer. En segundo lugar, las violaciones de alias de las que habla todo el mundo se trata de acceder a un objeto como un objeto diferente, no se trata simplemente de tener dos objetos con la misma dirección. Siempre y cuando no haya acceso de escritura de tipo , no hay problema. Esa fue la idea original. Más tarde, se permitió la tipología de los sindicatos.
ANT

133

Esta es la estricta regla de alias, que se encuentra en la sección 3.10 del estándar C ++ 03 (otras respuestas proporcionan una buena explicación, pero ninguna proporcionó la regla en sí):

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

  • el tipo dinámico del objeto
  • una versión calificada por cv del tipo dinámico del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada por cv del tipo dinámico del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión agregada o contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • a charo unsigned chartipo.

Texto de C ++ 11 y C ++ 14 (cambios enfatizados):

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

  • el tipo dinámico del objeto
  • una versión calificada por 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 con signo o sin signo correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada por cv del tipo dinámico del objeto,
  • un tipo de agregado o 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ático de un subaggregado o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • a charo unsigned chartipo.

Dos cambios fueron pequeños: glvalue en lugar de lvalue , y aclaración del caso agregado / unión.

El tercer cambio ofrece una garantía más sólida (relaja la fuerte regla de alias): el nuevo concepto de tipos similares que ahora son seguros para el alias.


También la redacción C (C99; ISO / IEC 9899: 1999 6.5 / 7; exactamente la misma redacción se utiliza en ISO / IEC 9899: 2011 §6.5 ¶7):

Un objeto tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos 73) u 88) :

  • un tipo compatible con el tipo efectivo del objeto,
  • una versión calificada de un tipo compatible con el tipo efectivo del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,
  • un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada del tipo efectivo del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión agregada o contenida), o
  • un tipo de personaje

73) u 88) La intención de esta lista es especificar aquellas circunstancias en las que un objeto puede o no tener alias.


77
Ben, como las personas a menudo se dirigen aquí, también me he permitido agregar la referencia al estándar C, en aras de la exhaustividad.
Kos

1
Mire el C89 Justificación cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf sección 3.3 que habla de ello.
phorgan1

2
Si uno tiene un valor l de un tipo de estructura, toma la dirección de un miembro y la pasa a una función que la utiliza como puntero al tipo de miembro, ¿se consideraría que tiene acceso a un objeto del tipo de miembro (legal), o un objeto del tipo de estructura (prohibido)? Una gran cantidad de código supone que es legal acceder a las estructuras de esa manera, y creo que muchas personas se quejarían de una regla que se entiende que prohíbe tales acciones, pero no está claro cuáles son las reglas exactas. Además, los sindicatos y las estructuras se tratan de la misma manera, pero las reglas sensatas para cada uno deberían ser diferentes.
supercat

2
@supercat: la forma en que está redactada la regla para las estructuras, el acceso real es siempre del tipo primitivo. Luego, el acceso a través de una referencia al tipo primitivo es legal porque los tipos coinciden, y el acceso a través de una referencia al tipo de estructura que lo contiene es legal porque está especialmente permitido.
Ben Voigt

2
@BenVoigt: No creo que la secuencia inicial común funcione a menos que los accesos se realicen a través de la unión. Ver goo.gl/HGOyoK para ver qué está haciendo gcc. Si acceder a un valor de tipo de unión mediante un valor de tipo de miembro (sin usar el operador de acceso de miembro de unión) fuera legal, entonces wow(&u->s1,&u->s2)debería ser legal incluso cuando se usa un puntero para modificar u, y eso negaría la mayoría de las optimizaciones la regla de alias fue diseñada para facilitar.
supercat

82

Nota

Esto se extrae de mi "¿Cuál es la regla de alias estricto y por qué nos importa?" redacción

¿Qué es el alias estricto?

En C y C ++, el alias tiene que ver con qué tipos de expresión se nos permite acceder a los valores almacenados. Tanto en C como en C ++, el estándar especifica qué tipos de expresión tienen permiso para alias de qué tipos. El compilador y el optimizador pueden asumir que seguimos estrictamente las reglas de alias, de ahí el término regla de alias estricto . Si intentamos acceder a un valor utilizando un tipo no permitido, se clasifica como comportamiento indefinido ( UB ). Una vez que tenemos un comportamiento indefinido, todas las apuestas se cancelan, los resultados de nuestro programa ya no son confiables.

Desafortunadamente, con violaciones de alias estrictas, a menudo obtendremos los resultados que esperamos, dejando la posibilidad de que una versión futura de un compilador con una nueva optimización rompa el código que pensamos que era válido. Esto no es deseable y es un objetivo que vale la pena comprender las estrictas reglas de alias y cómo evitar violarlas.

Para entender más acerca de por qué nos importa, discutiremos los problemas que surgen cuando se violan las estrictas reglas de alias, el tipo de punteo, ya que las técnicas comunes utilizadas en el tipo de punteo a menudo violan las estrictas reglas de alias y cómo escribir el juego de palabras correctamente.

Ejemplos preliminares

Veamos algunos ejemplos, luego podemos hablar sobre exactamente lo que dicen los estándares, examinar algunos ejemplos adicionales y luego ver cómo evitar el alias estricto y detectar las violaciones que pasamos por alto. Aquí hay un ejemplo que no debería sorprender ( ejemplo en vivo ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Tenemos un int * que apunta a la memoria ocupada por un int y este es un alias válido. El optimizador debe asumir que las asignaciones a través de ip podrían actualizar el valor ocupado por x .

El siguiente ejemplo muestra alias que conduce a un comportamiento indefinido ( ejemplo en vivo ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

En la función foo tomamos un int * y un float * , en este ejemplo llamamos a foo y establecemos ambos parámetros para que apunten a la misma ubicación de memoria que en este ejemplo contiene un int . Tenga en cuenta que reinterpret_cast le dice al compilador que trate la expresión como si tuviera el tipo especificado por su parámetro de plantilla. En este caso, le estamos diciendo que trate la expresión & x como si tuviera el tipo float * . Podemos esperar ingenuamente que el resultado del segundo cout sea 0 pero con la optimización habilitada usando -O2, tanto gcc como clang producen el siguiente resultado:

0
1

Lo cual puede no esperarse pero es perfectamente válido ya que hemos invocado un comportamiento indefinido. Un flotante no puede alias válidamente un objeto int . Por lo tanto, el optimizador puede asumir que la constante 1 almacenada al desreferenciar i será el valor de retorno ya que una tienda a través de f no podría afectar válidamente un objeto int . Conectar el código en el Explorador de compiladores muestra que esto es exactamente lo que está sucediendo ( ejemplo en vivo ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

El optimizador usando Análisis Alias Tipo-Based (TBAA) asume 1 serán devueltos y se mueve directamente el valor constante en el registro EAX que lleva el valor de retorno. TBAA usa las reglas de idiomas sobre qué tipos están permitidos para alias para optimizar cargas y tiendas. En este caso, TBAA sabe que un flotador no puede alias e int y optimiza la carga de i .

Ahora, al Libro de Reglas

¿Qué dice exactamente la norma que se nos permite y no se nos permite hacer? El lenguaje estándar no es sencillo, por lo que para cada elemento intentaré proporcionar ejemplos de código que demuestren el significado.

¿Qué dice el estándar C11?

El estándar C11 dice lo siguiente en la sección 6.5 Expresiones párrafo 7 :

A un objeto se le tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos: 88) - un tipo compatible con el tipo efectivo del objeto,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- una versión calificada de un tipo compatible con el tipo efectivo del objeto,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang tiene una extensión y también que permite asignar int * a int * sin firmar aunque no sean tipos compatibles.

- un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada del tipo efectivo del objeto,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión agregada o contenida), o

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- un tipo de personaje.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Lo que dice el borrador del estándar C ++ 17

El borrador del estándar C ++ 17 en la sección [basic.lval] párrafo 11 dice:

Si un programa intenta acceder al valor almacenado de un objeto a través de un valor gl diferente de uno de los siguientes tipos, el comportamiento no está definido: 63 (11.1): el tipo dinámico del objeto,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - una versión calificada por cv del tipo dinámico del objeto,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - un tipo similar (como se define en 7.5) al tipo dinámico del objeto,

(11.4) - un tipo que es el tipo con signo o sin signo correspondiente al tipo dinámico del objeto,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada por cv del tipo dinámico del objeto,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - un tipo de agregado o 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ático de un subaggregado o unión contenida),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8): un tipo char, unsigned char o std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Vale la pena señalar que el carácter firmado no está incluido en la lista anterior, esta es una diferencia notable de C que dice un tipo de carácter .

¿Qué es el tipo Punning?

Hemos llegado a este punto y podemos preguntarnos, ¿por qué querríamos alias? La respuesta generalmente es escribir un juego de palabras , a menudo los métodos utilizados violan estrictas reglas de alias.

A veces queremos evitar el sistema de tipos e interpretar un objeto como un tipo diferente. Esto se denomina punteo de tipo , para reinterpretar un segmento de memoria como otro tipo. La escritura de tipos es útil para tareas que desean acceder a la representación subyacente de un objeto para ver, transportar o manipular. Las áreas típicas que encontramos que se utilizan son los compiladores, la serialización, el código de red, etc.

Tradicionalmente, esto se ha logrado tomando la dirección del objeto, convirtiéndolo en un puntero del tipo con el que queremos reinterpretarlo y luego accediendo al valor, o en otras palabras, aliasing. Por ejemplo:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Como hemos visto anteriormente, este no es un alias válido, por lo que estamos invocando un comportamiento indefinido. Pero tradicionalmente los compiladores no aprovechaban las estrictas reglas de alias y este tipo de código generalmente funcionaba, desafortunadamente los desarrolladores se han acostumbrado a hacer las cosas de esta manera. Un método alternativo común para el tipo de punteo es a través de uniones, que es válido en C pero comportamiento indefinido en C ++ ( ver ejemplo en vivo ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Esto no es válido en C ++ y algunos consideran que el propósito de las uniones es únicamente para implementar tipos de variantes y sienten que el uso de uniones para el castigo de tipo es un abuso.

¿Cómo escribimos Pun correctamente?

El método estándar para la escritura de tipos en C y C ++ es memcpy . Esto puede parecer un poco pesado, pero el optimizador debe reconocer el uso de memcpy para la escritura de tipo y optimizarlo y generar un registro para registrar el movimiento. Por ejemplo, si sabemos que int64_t tiene el mismo tamaño que el doble :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

podemos usar memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

En un nivel de optimización suficiente, cualquier compilador moderno decente genera un código idéntico al método reinterpret_cast mencionado anteriormente o al método de unión para el tipo punning . Examinando el código generado, vemos que usa solo registrar mov ( ejemplo de Live Compiler Explorer ).

C ++ 20 y bit_cast

En C ++ 20 podemos obtener bit_cast ( implementación disponible en el enlace de la propuesta ) que proporciona una forma simple y segura de escribir juegos de palabras, además de ser utilizable en un contexto constexpr.

El siguiente es un ejemplo de cómo usar bit_cast para escribir pun un int no firmado para flotar , ( verlo en vivo ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

En el caso de que los tipos To y From no tengan el mismo tamaño, requiere que usemos una estructura intermedia15. Usaremos una estructura que contenga una matriz de caracteres sizeof (unsigned int) (se supone que 4 bytes unsigned int ) es el tipo From y unsigned int como el tipo To . :

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Es lamentable que necesitemos este tipo intermedio, pero esa es la restricción actual de bit_cast .

Capturando violaciones de alias estrictas

No tenemos muchas herramientas buenas para detectar el alias estricto en C ++, las herramientas que tenemos detectarán algunos casos de violaciones de alias estricto y algunos casos de cargas y tiendas desalineadas.

gcc usando la bandera -fstrict-aliasing y -Wstrict-aliasing puede detectar algunos casos, aunque no sin falsos positivos / negativos. Por ejemplo, los siguientes casos generarán una advertencia en gcc ( verlo en vivo ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

aunque no captará este caso adicional ( verlo en vivo ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Aunque el sonido metálico permite estas banderas, aparentemente no implementa las advertencias.

Otra herramienta que tenemos disponible es ASan, que puede detectar cargas y tiendas desalineadas. Aunque estas no son violaciones de alias estrictamente directas, son un resultado común de violaciones de alias estrictas. Por ejemplo, los siguientes casos generarán errores de tiempo de ejecución cuando se crean con clang usando -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

La última herramienta que recomendaré es específica de C ++ y no estrictamente una herramienta, sino una práctica de codificación, no permita conversiones de estilo C. Tanto gcc como clang producirán un diagnóstico para los lanzamientos de estilo C usando -Wold-style-cast . Esto forzará a cualquier juego de palabras de tipo indefinido a usar reinterpret_cast, en general reinterpret_cast debería ser una bandera para una revisión más detallada del código. También es más fácil buscar en su base de código reinterpret_cast para realizar una auditoría.

Para C tenemos todas las herramientas ya cubiertas y también tenemos tis-interpreter, un analizador estático que analiza exhaustivamente un programa para un gran subconjunto del lenguaje C. Dada una versión en C del ejemplo anterior donde el uso de -fstrict-aliasing pierde un caso ( verlo en vivo )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter es capaz de atrapar a los tres, el siguiente ejemplo invoca tis-kernal como tis-interpreter (la salida se edita por brevedad):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Finalmente está TySan, que actualmente está en desarrollo. Este desinfectante agrega información de verificación de tipos en un segmento de memoria secundaria y verifica los accesos para ver si violan las reglas de alias. La herramienta debería poder detectar todas las infracciones de alias pero puede tener una gran sobrecarga de tiempo de ejecución.


Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Bhargav Rao

3
Si pudiera, +10, bien escrito y explicado, también desde ambos lados, escritores de compiladores y programadores ... la única crítica: sería bueno tener contraejemplos anteriores, para ver lo que está prohibido por el estándar, no es obvio tipo de :-)
Gabriel

2
Muy buena respuesta. Solo lamento que los ejemplos iniciales se dan en C ++, lo que hace que sea difícil de seguir para personas como yo que solo conocen o se preocupan por C y no tienen idea de lo que reinterpret_castpodría hacer o lo que coutpodría significar. (Está bien mencionar C ++, pero la pregunta original era sobre C y IIUC, estos ejemplos podrían escribirse igualmente en C.)
Gro-Tsen

Con respecto a la escritura de tipos: así que si escribo una matriz de algún tipo X en el archivo, luego leo de ese archivo esta matriz en la memoria apuntada con void *, entonces lanzo ese puntero al tipo real de datos para usarlo, eso es comportamiento indefinido?
Michael IV

44

El alias estricto no se refiere solo a los punteros, sino que también afecta a las referencias, escribí un artículo al respecto para la wiki de desarrollador de impulso y fue tan bien recibido que lo convertí en una página en mi sitio web de consultoría. Explica completamente qué es, por qué confunde tanto a las personas y qué hacer al respecto. Libro blanco de alias estricto . En particular, explica por qué las uniones son un comportamiento arriesgado para C ++, y por qué usar memcpy es la única solución portátil en C y C ++. Espero que esto sea útil.


3
"El alias estricto no se refiere solo a los punteros, sino que también afecta a las referencias " En realidad, se refiere a los valores . " usar memcpy es la única solución portátil " ¡Escucha!
curioso

55
Buen papel Mi opinión: (1) este aliasing-'problem 'es una reacción exagerada a la mala programación, tratando de proteger al mal programador de sus malos hábitos. Si el programador tiene buenos hábitos, este alias es solo una molestia y los controles se pueden desactivar de forma segura. (2) La optimización del lado del compilador solo debe realizarse en casos conocidos y, en caso de duda, debe seguir estrictamente el código fuente; Obligar al programador a escribir código para atender las idiosincrasias del compilador es, simplemente, incorrecto. Peor aún para que sea parte del estándar.
slashmais

44
@slashmais (1) " es una reacción exagerada a la mala programación " Tonterías. Es un rechazo de los malos hábitos. ¿Haces eso? Usted paga el precio: ¡no hay garantía para usted! (2) Casos bien conocidos? ¿Cuáles? ¡La estricta regla de alias debería ser "bien conocida"!
curioso

55
@curiousguy: Después de aclarar algunos puntos de confusión, está claro que el lenguaje C con las reglas de alias hace imposible que los programas implementen agrupaciones de memoria independientes del tipo. Algunos tipos de programas pueden funcionar con malloc / free, pero otros necesitan una lógica de administración de memoria mejor adaptada a las tareas en cuestión. Me pregunto por qué la lógica de C89 utilizó un ejemplo tan malo de la razón de la regla de alias, ya que su ejemplo hace que parezca que la regla no planteará ninguna dificultad importante para realizar una tarea razonable.
supercat

55
@curiousguy, la mayoría de los conjuntos de compiladores incluyen -fstrict-aliasing como predeterminado en -O3 y este contrato oculto se impone a los usuarios que nunca han oído hablar de TBAA y escribieron código como podría hacerlo un programador de sistemas. No pretendo parecer falso a los programadores del sistema, pero este tipo de optimización debería dejarse fuera de la opción predeterminada de -O3 y debería ser una optimización opcional para aquellos que saben lo que es TBAA. No es divertido mirar el 'error' del compilador que resulta ser un código de usuario que viola TBAA, especialmente rastrear la violación del nivel de fuente en el código de usuario.
kchoi

34

Como anexo a lo que Doug T. ya escribió, aquí hay un caso de prueba simple que probablemente lo desencadena con gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Compilar con gcc -O2 -o check check.c. Usualmente (con la mayoría de las versiones de gcc que probé) esto genera un "problema de alias estricto", porque el compilador supone que "h" no puede ser la misma dirección que "k" en la función "verificar". Por eso, el compilador optimiza el if (*h == 5)ausente y siempre llama a printf.

Para aquellos que estén interesados ​​aquí está el código de ensamblador x64, producido por gcc 4.6.3, que se ejecuta en ubuntu 12.04.2 para x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Entonces, la condición if desapareció por completo del código del ensamblador.


si agrega un segundo corto * j a check () y lo usa (* j = 7), entonces la optimización desaparece ya que ggc no lo hace si h y j no apuntan realmente al mismo valor. Sí, la optimización es realmente inteligente.
philippe lhardy

2
Para hacer las cosas más divertidas, use punteros a tipos que no son compatibles pero que tienen el mismo tamaño y representación (en algunos sistemas, por ejemplo, long long*y int64_t*). Uno podría esperar que un compilador sensata debería reconocer que una long long*y int64_t*podría tener acceso a la misma de almacenamiento si están almacenados de forma idéntica, pero dicho tratamiento ya no está de moda.
supercat

Grr ... x64 es una convención de Microsoft. Use amd64 o x86_64 en su lugar.
SS Anne

Grr ... x64 es una convención de Microsoft. Use amd64 o x86_64 en su lugar.
SS Anne

17

La escritura de tipos mediante el uso de punteros (en lugar de usar una unión) es un ejemplo importante de romper el alias estricto.


1
Vea mi respuesta aquí para ver las citas relevantes, especialmente las notas al pie de página, pero los juegos de palabras a través de los sindicatos siempre se han permitido en C, aunque al principio estaba mal redactado. Usted quiere aclarar su respuesta.
Shafik Yaghmour

@ShafikYaghmour: C89 claramente permitió a los implementadores seleccionar los casos en los que reconocerían o no útilmente el tipo de castigo a través de los sindicatos. Una implementación podría, por ejemplo, especificar que para que una escritura en un tipo seguida de una lectura de otro sea reconocida como punción de tipo, si el programador realizó una de las siguientes acciones entre la escritura y la lectura : (1) evalúe un valor que contenga el tipo de unión [tomar la dirección de un miembro calificaría, si se hace en el punto correcto de la secuencia]; (2) convierta un puntero a un tipo en un puntero al otro y acceda a través de ese ptr.
supercat

@ShafikYaghmour: una implementación también podría especificar, por ejemplo, que el tipo de punteo entre valores enteros y de punto flotante solo funcionaría de manera confiable si el código ejecutara una fpsync()directiva entre escribir como fp y leer como int o viceversa [en implementaciones con tuberías y cachés de enteros y FPU por separado , tal directiva podría ser costosa, pero no tan costosa como hacer que el compilador realice dicha sincronización en cada acceso de unión]. O una implementación podría especificar que el valor resultante nunca será utilizable, excepto en circunstancias que utilizan secuencias iniciales comunes.
supercat

@ShafikYaghmour: Según C89, las implementaciones podrían prohibir la mayoría de las formas de puntería de tipo, incluso a través de uniones, pero la equivalencia entre punteros a uniones y punteros a sus miembros implicaba que se permitía la tipificación en las implementaciones que no lo prohibían expresamente .
supercat

17

Según la justificación de C89, los autores de la Norma no querían exigir que los compiladores dieran código como:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

se debe exigir que vuelva a cargar el valor de xentre la asignación y la declaración de retorno para permitir la posibilidad de que ppueda apuntar x, y la asignación de *ppuede alterar en consecuencia el valor de x. La noción de que un compilador debería tener derecho a suponer que no habrá alias en situaciones como las anteriores no fue controvertida.

Desafortunadamente, los autores del C89 escribieron su regla de una manera que, si se lee literalmente, haría que incluso la siguiente función invoque Comportamiento indefinido:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

porque usa un valor de tipo l intpara acceder a un objeto de tipo struct S, y intno está entre los tipos que se pueden usar para acceder a unstruct S . Debido a que sería absurdo tratar todo uso de miembros de estructuras y uniones que no sean del tipo de caracteres como Comportamiento indefinido, casi todos reconocen que hay al menos algunas circunstancias en las que se puede usar un valor de un tipo para acceder a un objeto de otro tipo . Lamentablemente, el Comité de Normas C no ha podido definir cuáles son esas circunstancias.

Gran parte del problema es el resultado del Informe de defectos # 028, que preguntó sobre el comportamiento de un programa como:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

El Informe de defectos n. ° 28 establece que el programa invoca Comportamiento indefinido porque la acción de escribir un miembro de unión de tipo "double" y leer uno de tipo "int" invoca un comportamiento definido por la implementación. Tal razonamiento no tiene sentido, pero forma la base de las reglas de Tipo efectivo que complican innecesariamente el lenguaje sin hacer nada para abordar el problema original.

La mejor manera de resolver el problema original probablemente sería tratar la nota al pie de página sobre el propósito de la regla como si fuera normativa, y hacer que la regla no se pueda hacer cumplir, excepto en los casos que realmente involucran accesos conflictivos usando alias. Dado algo como:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

No hay conflicto en el interior inc_intporque todos los accesos al almacenamiento al que se accede *pse realizan con un valor de tipo l int, y no hay conflicto testporque pse deriva visiblemente de un struct S, y para la próxima vez que sse use, todos los accesos a ese almacenamiento se realizarán alguna vez a través pya habrá sucedido.

Si el código fuera cambiado ligeramente ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Aquí, existe un conflicto de alias py el acceso a s.xen la línea marcada porque en ese punto de ejecución existe otra referencia que se utilizará para acceder al mismo almacenamiento .

Si el Informe de defectos 028 decía que el ejemplo original invocaba a UB debido a la superposición entre la creación y el uso de los dos punteros, eso habría aclarado mucho las cosas sin tener que agregar "Tipos efectivos" u otra complejidad similar.


Bien dicho, sería interesante leer una especie de propuesta que fuera más o menos "lo que el comité de normas podría haber hecho" que lograra sus objetivos sin introducir tanta complejidad.
jrh

1
@jrh: Creo que sería bastante simple. Reconozca que 1. Para que se produzca un alias durante una ejecución particular de una función o bucle, se deben usar dos punteros o valores diferentes durante esa ejecución para abordar el mismo almacenamiento en un modo conflictivo; 2. Reconozca que en contextos donde un puntero o valor se deriva de manera visible de otro, un acceso al segundo es un acceso al primero; 3. Reconozca que la regla no está destinada a aplicarse en casos que en realidad no implican aliasing.
supercat

1
Las circunstancias exactas en las que un compilador reconoce un valor l recién derivado puede ser un problema de calidad de implementación, pero cualquier compilador remotamente decente debería ser capaz de reconocer formas que gcc y clang ignoran deliberadamente.
supercat

11

Después de leer muchas de las respuestas, siento la necesidad de agregar algo:

El alias estricto (que describiré en un momento) es importante porque :

  1. El acceso a la memoria puede ser costoso (en cuanto al rendimiento), por lo que los datos se manipulan en los registros de la CPU antes de volver a escribirse en la memoria física.

  2. Si los datos en dos registros de CPU diferentes se escribirán en el mismo espacio de memoria, no podemos predecir qué datos "sobrevivirán" cuando codifiquemos en C.

    En el ensamblaje, donde codificamos la carga y descarga de los registros de la CPU manualmente, sabremos qué datos permanecen intactos. Pero C (afortunadamente) abstrae este detalle.

Dado que dos punteros pueden apuntar a la misma ubicación en la memoria, esto podría resultar en un código complejo que maneja posibles colisiones .

Este código adicional es lento y perjudica el rendimiento ya que realiza operaciones adicionales de lectura / escritura de memoria que son más lentas y (posiblemente) innecesarias.

La regla de alias estricto nos permite evitar el código de máquina redundante en los casos en que debería ser seguro asumir que dos punteros no apuntan al mismo bloque de memoria (ver también la restrictpalabra clave).

El alias estricto indica que es seguro asumir que los punteros a diferentes tipos apuntan a diferentes ubicaciones en la memoria.

Si un compilador nota que dos punteros apuntan a diferentes tipos (por ejemplo, an int *y a float *), asumirá que la dirección de memoria es diferente y no protegerá contra colisiones de direcciones de memoria, lo que resulta en un código de máquina más rápido.

Por ejemplo :

Asumamos la siguiente función:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Para manejar el caso en el que a == b(ambos punteros apuntan a la misma memoria), necesitamos ordenar y probar la forma en que cargamos datos de la memoria a los registros de la CPU, por lo que el código podría terminar así:

  1. carga ay bde memoria.

  2. añadir aa b.

  3. guardar b y recargar a .

    (guardar desde el registro de la CPU en la memoria y cargar desde la memoria en el registro de la CPU).

  4. añadir ba a.

  5. guardar a(desde el registro de la CPU) en la memoria.

El paso 3 es muy lento porque necesita acceder a la memoria física. Sin embargo, se requiere para proteger contra instancias donde ay bapuntar a la misma dirección de memoria.

El alias estricto nos permitiría evitar esto al decirle al compilador que estas direcciones de memoria son claramente diferentes (lo que, en este caso, permitirá una optimización aún mayor que no se puede realizar si los punteros comparten una dirección de memoria).

  1. Esto se puede decir al compilador de dos maneras, utilizando diferentes tipos para señalar. es decir:

    void merge_two_numbers(int *a, long *b) {...}
  2. Usando la restrictpalabra clave. es decir:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Ahora, al cumplir la regla de Alias ​​estricto, se puede evitar el paso 3 y el código se ejecutará significativamente más rápido.

De hecho, al agregar la restrictpalabra clave, toda la función podría optimizarse para:

  1. carga ay bde memoria.

  2. añadir aa b.

  3. guardar resultado tanto para acomo para b.

Esta optimización no podría haberse hecho antes, debido a la posible colisión (dónde ay bse triplicaría en lugar de duplicarse).


con la palabra clave restrict, en el paso 3, ¿no debería ser guardar el resultado en 'b' solamente? Parece que el resultado de la suma también se almacenará en 'a'. ¿Es necesario volver a cargar 'b'?
NeilB

1
@NeilB - Yap tienes razón. Solo estamos guardando b(no volviendo a cargarlo) y volviendo a cargar a. Espero que sea más claro ahora.
Myst

El alias basado en el tipo puede haber ofrecido algunos beneficios antes restrict, pero creo que este último en la mayoría de las circunstancias sería más efectivo, y aflojar algunas restricciones registerle permitiría completar algunos de los casos en los restrictque no ayudaría. No estoy seguro de que alguna vez fue "importante" tratar el Estándar como una descripción completa de todos los casos en que los programadores deben esperar que los compiladores reconozcan la evidencia de alias, en lugar de simplemente describir los lugares donde los compiladores deben suponer alias, incluso cuando no existe evidencia particular de ello .
supercat

Tenga en cuenta que aunque la carga desde la RAM principal es muy lenta (y puede detener el núcleo de la CPU durante mucho tiempo si las siguientes operaciones dependen del resultado), la carga desde la caché L1 es bastante rápida, y también lo está escribiendo en una línea de caché que recientemente se estaba escribiendo a por el mismo núcleo. Por lo tanto, todos, excepto la primera lectura o escritura en una dirección, generalmente serán razonablemente rápidos: la diferencia entre el acceso de registro / memoria adicional es menor que la diferencia entre el acceso de memoria caché / sin caché.
curioso

@curiousguy: aunque tienes razón, "rápido" en este caso es relativo. El caché L1 es probablemente un orden de magnitud más lento que los registros de la CPU (creo que más de 10 veces más lento). Además, la restrictpalabra clave minimiza no solo la velocidad de las operaciones, sino también su número, lo que podría ser significativo ... Quiero decir, después de todo, la operación más rápida es ninguna operación :)
Myst

6

El alias estricto no permite diferentes tipos de puntero a los mismos datos.

Este artículo debería ayudarlo a comprender el problema con todo detalle.


44
Puede alias entre referencias y entre una referencia y un puntero también. Vea mi tutorial dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

44
Se permite tener diferentes tipos de puntero a los mismos datos. Donde entra en juego el alias estricto es cuando la misma ubicación de memoria se escribe a través de un tipo de puntero y se lee a través de otro. Además, se permiten algunos tipos diferentes (por ejemplo, intuna estructura que contiene un int).
MM

-3

Técnicamente en C ++, la estricta regla de alias probablemente nunca sea aplicable.

Tenga en cuenta la definición de indirección ( * operador ):

El operador unario * realiza indirectamente: la expresión a la que se aplica será un puntero a un tipo de objeto, o un puntero a un tipo de función y el resultado es un valor l que se refiere al objeto o función al que apunta la expresión .

También de la definición de glvalue

Un glvalue es una expresión cuya evaluación determina la identidad de un objeto, (... snip)

Entonces, en cualquier rastreo de programa bien definido, un valor de gl se refiere a un objeto. Por lo tanto, la llamada regla de alias estricto no se aplica, nunca. Esto puede no ser lo que los diseñadores querían.


44
El estándar C utiliza el término "objeto" para referirse a una serie de conceptos diferentes. Entre ellos, una secuencia de bytes que se asignan exclusivamente para algún propósito, una referencia no necesariamente exclusiva a una secuencia de bytes a / desde la cual se puede escribir o leer un valor de un tipo particular , o una referencia que realmente tiene ha sido o será accedido en algún contexto. No creo que haya una forma sensata de definir el término "Objeto" que sea coherente con la forma en que el Estándar lo utiliza.
supercat

@supercat Incorrecto. A pesar de su imaginación, en realidad es bastante consistente. En ISO C se define como "región de almacenamiento de datos en el entorno de ejecución, cuyo contenido puede representar valores". En ISO C ++ hay una definición similar. Su comentario es aún más irrelevante que la respuesta porque todo lo que mencionó son formas de representación para referir el contenido de los objetos , mientras que la respuesta ilustra el concepto de C ++ (glvalue) de un tipo de expresiones que se relacionan estrechamente con la identidad de los objetos. Y todas las reglas de alias son básicamente relevantes para la identidad pero no para el contenido.
FrankHB

1
@FrankHB: Si uno declara int foo;, ¿a qué accede la expresión lvalue *(char*)&foo? ¿Es eso un objeto de tipo char? ¿Ese objeto llega a existir al mismo tiempo que foo? ¿Escribiría para foocambiar el valor almacenado de ese objeto de tipo mencionado anteriormente char? Si es así, ¿hay alguna regla que permita characceder al valor almacenado de un objeto de tipo utilizando un valor de tipo l int?
supercat

@FrankHB: en ausencia de 6.5p7, uno podría simplemente decir que cada región de almacenamiento contiene simultáneamente todos los objetos de todo tipo que podrían caber en esa región de almacenamiento, y que acceder a esa región de almacenamiento accede simultáneamente a todos ellos. Sin embargo, interpretar de esta manera el uso del término "objeto" en 6.5p7 prohibiría hacer cualquier cosa con valores de tipo no característico, lo que claramente sería un resultado absurdo y anularía totalmente el propósito de la regla. Además, el concepto de "objeto" utilizado en cualquier lugar que no sea 6.5p6 tiene un tipo de tiempo de compilación estático, pero ...
supercat

1
sizeof (int) es 4, la declaración int i;crea cuatro objetos de cada tipo de carácter in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` y i. Finalmente, no hay nada en el Estándar que permita que incluso un volatilepuntero calificado acceda a registros de hardware que no cumplan con la definición de "objeto".
supercat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.