¿Cuál es la estricta regla de aliasing?


Al preguntar sobre el comportamiento común indefinido en C, las almas más iluminadas de lo que me referí a la estricta regla de aliasing.
¿De qué están hablando?

Author: Community, 2008-09-19

11 answers

Una situación típica que se encuentra con problemas estrictos de aliasing es cuando se superpone una estructura (como un msg de dispositivo/red) en un búfer del tamaño de palabra de su sistema (como un puntero a uint32_ts o uint16_ts). Cuando superpone una estructura en un búfer de este tipo, o un búfer en una estructura de este tipo a través de pointer casting, puede violar fácilmente las estrictas reglas de aliasing.

Así que en este tipo de configuración, si quiero enviar un mensaje a algo tendría que tener dos punteros incompatibles apuntando a lo mismo un pedazo de memoria. Entonces podría ingenuamente codificar algo como esto:

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 aliasing 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 el párrafo C 2011 6.5 71 es un comportamiento indefinido. Desafortunadamente, todavía puede codificar de esta manera, tal vez reciba algunas advertencias, haga que se compile 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 a veces no.)

Para ver por qué este comportamiento no está definido, tenemos que pensar en lo que la estricta regla de aliasing compra al compilador. Básicamente, con esta regla, no tiene que pensar en insertar instrucciones para actualizar el contenido de buff cada ejecución del bucle. En su lugar, al optimizar, con algunas suposiciones molestamente no cumplidas sobre el aliasing, 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 el alias estricto, el compilador tenía que vivir en un estado de paranoia de que el contenido de buff pudiera cambiar en cualquier momento desde cualquier lugar por cualquiera. Así que para obtener una ventaja de rendimiento adicional, y asumiendo que la mayoría de las personas no escriben punteros de juego de palabras, se introdujo la estricta regla de aliasing.

Tenga en cuenta, si usted piensa que el ejemplo es artificial, esto incluso podría suceder si está pasando un búfer a otra función que realiza el envío por usted, si en su lugar 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 insertar SendMessage y puede o no decidir cargar o no cargar buff de nuevo. Si SendMessage es parte de otra API que se compila por separado, probablemente tenga instrucciones para cargar el contenido de buff. Entonces una vez más, tal vez estás en C++ y esta es una implementación de encabezado con plantilla que el compilador cree que puede insertar. O tal vez es algo que escribiste en tu .c presentar para su propia conveniencia. De todos modos un comportamiento indefinido podría seguir. Incluso cuando sabemos algo de lo que está sucediendo bajo el capó, sigue siendo una violación de la regla, por lo que no se garantiza un comportamiento bien definido. Así que simplemente envolviendo en una función que toma nuestra palabra delimitada buffer no necesariamente ayudar.

¿Cómo puedo evitar esto?

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

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Puede deshabilitar el alias estricto en su compilador (f[no-]strict-aliasing 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 char y unsigned char). Siempre se asume que char* alias de otros tipos. Sin embargo, esto no funcionará de la otra manera: no hay ninguna suposición de que su estructura se apoda a un búfer de caracteres.

Cuidado con el principiante

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

Nota al pie

1 Los tipos a los que C 2011 6.5 7 permite acceder a un lvalue 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 firmado o sin firmar correspondiente al tipo efectivo del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada de la tipo efectivo del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluyendo, recursivamente, un miembro de un subagregado o unión contenida), o
  • un tipo de carácter.
 496
Author: Doug T.,
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
2018-06-09 14:32:04

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

Del artículo:

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

Así que básicamente si tienes un int* apuntando a alguna memoria conteniendo un int y luego apuntas un float* a esa memoria y la usas como un float rompes 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.

 219
Author: Niall,
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-10-16 14:46:56

Esta es la regla de alias estricta, 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 lvalue distinto de 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 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 miembros (incluido, recursivamente, un miembro 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,
  • tipo char o unsigned char.

C++11 y C++14 redacción (cambios hincapié):

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue de otro tipo que no sea uno de los siguientes, 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 dinámico tipo 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 un subagregado o unión confinada),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • tipo char o unsigned char.

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

El tercer cambio hace una garantía más fuerte (relaja la regla de aliasing fuerte): El nuevo concepto de tipos similares que ahora son seguros para apodo.


También el C texto (C99; ISO / IEC 9899: 1999 6.5 / 7; el mismo texto exacto se utiliza en ISO / IEC 9899:2011 §6.5 ¶7):

Un objeto tendrá acceso a su valor almacenado solo por un lvalue expresión que tiene uno de los siguientes tipos 73) o 88):

  • un tipo compatible con el tipo efectivo del objeto,
  • una versión cualificada de un tipo compatible con el tipo efectivo de el objeto,
  • a tipo que es el tipo firmado o sin firmar correspondiente a la tipo efectivo del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a un versión calificada del tipo efectivo del objeto,
  • un tipo agregado o unión que incluye uno de los mencionados anteriormente tipos entre sus miembros (incluyendo, recursivamente, un miembro de un subagregado o unión confinada), o
  • un tipo de carácter.

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

 125
Author: Ben Voigt,
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-10-19 22:30:34

El alias estricto no se refiere solo a punteros, también afecta a las referencias, escribí un artículo sobre él para el wiki de desarrolladores de boost 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 la gente 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 portable tanto en C como en C++. Esperar esto es útil.

 40
Author: Patrick,
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
2011-06-19 23:46:55

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

Comprobar.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 . Normalmente (con la mayoría de las versiones de gcc que probé) esto genera un "problema de alias estricto", porque el compilador asume que "h" no puede ser la misma dirección que "k" en la función "check". Debido a esto, el compilador optimiza el if (*h == 5) away y siempre llama al printf.

Para aquellos que están interesados aquí está el x64 código ensamblador, 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

Así que la condición if ha desaparecido completamente del código ensamblador.

 31
Author: Ingo Blackman,
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
2013-05-14 02:37:04

Nota

Esto está extraído de mi "¿Cuál es la estricta Regla de Aliasing y Por qué nos importa?" write-up.

¿Qué es el alias estricto?

En C y C++ el aliasing tiene que ver con los tipos de expresión a través de los cuales se nos permite acceder a los valores almacenados. Tanto en C como en C++ el estándar especifica qué tipos de expresión se permiten 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 estricta de aliasing. 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 estrictas de aliasing, 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 es indeseable y es un objetivo que vale la pena entender las reglas estrictas de aliasing y cómo evitar violarlas.

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

Ejemplos preliminares

Veamos algunos ejemplos, entonces podemos hablar exactamente de lo que dicen los estándares, examine algunos ejemplos más y luego vea cómo evitar el alias estricto y las violaciones de captura que nos perdimos. 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 aliasing que conduce a undefined comportamiento ( 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 una int* y a float*, en este ejemplo vamos a llamar a foo y el conjunto de ambos parámetros para que apunte a la misma ubicación de memoria que en este ejemplo contiene un int. Tenga en cuenta que el reinterpret_cast le está diciendo 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 ser esperado pero es perfectamente válido ya que hemos invocado un comportamiento indefinido. Un float no puede alias válidamente un objeto int . Por lo tanto, el optimizador puede asumir la constante 1 almacenada cuando la desreferenciación iserá el valor devuelto ya que un almacén a través de fno podría afectar válidamente a un objeto int. Conectar el código en el Compiler Explorer 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 que usa El Análisis de Alias basado en Tipos (TBAA) asume 1 se devuelve y mueve directamente el valor constante en register eax que lleva el valor devuelto. TBAA utiliza el idiomas reglas sobre qué tipos se permiten alias para optimizar cargas y tiendas. En este caso TBAA sabe que un float no puede alias y int y optimiza la carga de i .

Ahora, al Libro de Reglas{[29]]}

¿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é hace el C11 ¿lo normal?

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

Un objeto tendrá acceso a su valor almacenado solo por 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 firmado o sin firmar 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* sin signo a int* aunque no sean tipos compatibles.

- un tipo que es el tipo firmado o sin firmar 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 agregado o tipo de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluyendo, recursivamente, un miembro de un subagregado o sindicato contenido), 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 carácter.

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 [básico.lval] el párrafo 11 dice:

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los tipos siguientes el comportamiento es indefinido: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 firmado o sin firmar 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) - tipo a es decir, el tipo firmado o sin firmar correspondiente a una versión calificada 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 agregado o de unión que incluye uno de los tipos antes mencionados entre sus elementos o miembros de datos no estáticos (incluyendo, recursivamente, un elemento o miembro de datos no estáticos de un subagregado 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) de la dinámica tipo 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
}

Worth noting signed charno está incluido en la lista anterior, esta es una diferencia notable de Cque dice un tipo de carácter.

¿Qué es el Tipo de juego de palabras

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

A veces queremos eludir el sistema de tipos e interpretar un objeto como un tipo diferente. Esto se llama tipo descriptivo, para reinterpretar un segmento de memoria como otro tipo. Type punning 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 son compiladores, serialización, código de red, etc…

Tradicionalmente esto se ha logrado tomando la dirección del objeto, lanzándolo a un puntero del tipo que queremos reinterpretar como y luego accediendo al valor, o en otras palabras mediante 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 aliasing y este tipo de código generalmente solo funcionaba, los desarrolladores desafortunadamente tienen acostumbrado a hacer las cosas de esta manera. Un método alternativo común para el juego de palabras 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 unions es únicamente para implementar tipos de variantes y sienten que usar unions para el juego de tipos es un abuso.

¿Cómo escribimos el Juego de palabras correctamente?

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

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 código idéntico al mencionado anteriormente método reinterpret_casto método unionpara tipo punning. Examinando el código generado vemos que utiliza simplemente register mov ( live Compiler Explorer Example).

C++20 y bit_cast

En C++20 podemos ganar bit_cast (implementación disponible en link desde proposal) que proporciona una forma sencilla y segura de escribir un juego de palabras, además de ser utilizable en un contexto constexpr.

El siguiente es un ejemplo de cómo use bit_cast para escribir pun a unsigned int to float, (véalo en vivo):

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

En el caso de que los tipos A y De no tengan el mismo tamaño, nos obliga a usar una estructura intermedia15. Vamos a utilizar una estructura que contiene a sizeof( unsigned int ) matriz de caracteres (asume 4 bytes unsigned int) para ser el De tipo y unsigned int como A tipo.:

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 desafortunado que necesitemos este tipo intermedio, pero esa es la restricción actual de bit_cast.

Captura de Violaciones Estrictas de Alias

No tenemos muchas herramientas buenas para capturar alias estrictos en C++, las herramientas que tenemos detectarán algunos casos de violaciones de alias estrictos y algunos casos de cargas y almacenes desalineados.

Gcc usando el indicador -fstrict-aliasing y -Wstrict-aliasing puede coger algunos casos, aunque no sin falsos positivos / negativos. Por ejemplo, los siguientes casos generarán una advertencia en gcc ( see it live):

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 atrapará este caso adicional (verlo en vivo):

int *p;

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

Aunque clang permite estas banderas, aparentemente no implementa las advertencias.

Otra herramienta que tenemos a nuestra disposición es ASan, que puede capturar cargas y almacenes desalineados. Aunque estos no son directamente violaciones estrictas de aliasing son un resultado común de violaciones estrictas de aliasing. Por ejemplo, los siguientes casos generarán errores de tiempo de ejecución cuando se compila 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 permitir casts de estilo C. Tanto gcc como clang producirán un diagnóstico para moldes de estilo C usando -Wold-style-cast. Esto obligará a usar cualquier juego de palabras de tipo indefinido reinterpret_cast, en general reinterpret_cast debería ser un indicador 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. Dado un C verions del ejemplo anterior donde usando - fstrict-aliasing falta un caso ( véalo 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 capturar 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 está actualmente en desarrollo. Este desinfectante agrega información de comprobación de tipos en un segmento de memoria de sombra y comprueba los accesos para ver si violan las reglas de alias. La herramienta potencialmente debería ser capaz de capturar todas las violaciones de alias, pero puede tener un gran tiempo de ejecución sobrecarga.

 23
Author: Shafik Yaghmour,
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
2018-08-20 13:14:43

Type punning vía pointer casts (a diferencia de usar una unión) es un ejemplo importante de romper el alias estricto.

 15
Author: Chris Jester-Young,
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
2014-07-06 18:31:53

De acuerdo con la justificación de C89, los autores del Estándar no querían exigir que los compiladores dieran código como:

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

Debería ser necesario volver a cargar el valor de x entre la asignación y la instrucción return para permitir la posibilidad de que p podría apuntar a x, y la asignación a *p puede, en consecuencia alterar el valor de x. La noción de que un compilador debe tener derecho a suponer que no habrá aliasing en situaciones como la arriba no fue controvertido.

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 un Comportamiento indefinido:

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

Porque utiliza un lvalue de tipo int para acceder a un objeto de tipo struct S, y int no es uno de los tipos que se pueden utilizar accediendo a un struct S. Porque sería absurdo tratar todo uso de miembros sin carácter de estructuras y sindicatos como un Comportamiento Indefinido, casi todo el mundo reconoce que hay al menos algunas circunstancias en las que se puede usar un lvalue de un tipo para acceder a un objeto de otro tipo. Lamentablemente, el Comité de Normas C no ha definido cuáles son esas circunstancias.

Gran parte del problema es el resultado del Informe de defectos #028, que preguntaba 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 #28 indica que el programa invoca un Comportamiento Indefinido debido a la acción de escribir a un miembro del sindicato de tipo "double "y la lectura de uno de tipo" int " invoca el comportamiento definido por la Implementación. Tal razonamiento es absurdo, pero forma la base para las reglas de Tipo Efectivas que complican innecesariamente el lenguaje mientras no hacen nada para abordar el problema original.

La mejor manera de resolver el problema original sería probablemente tratar la nota de pie de página sobre el propósito de la regla como si fuera normativa, y hecho la norma es inaplicable, excepto en los casos en que de hecho se trate de conflictos accesos mediante 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 dentro de inc_int porque todos los accesos al almacenamiento al que se accede a través de *p se realizan con un lvalue de tipo int, y no hay conflicto en test porque p se deriva visiblemente de un struct S, y para la próxima vez que se use s, todos los accesos a ese almacenamiento que se realizarán a través de p ya habrán ocurrido.

Si el código fue 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í, hay un conflicto de aliasing entre p y el acceso a s.x en la línea marcada porque en ese punto de ejecución existe otra referencia que se utilizará para acceder al mismo almacenamiento.

Tenía Reporte de Defectos 028 dijo que el ejemplo original invoca UB debido a la superposición entre la creación y el uso de los dos punteros, que hubiera hecho las cosas mucho más claras, sin tener que añadir Eficaces "Tipos" o de otras complejidad.

 10
Author: supercat,
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
2018-03-10 00:42:31

Después de leer muchas de las respuestas, siento la necesidad de añadir algo: {[29]]}

El alias estricto (que describiré en un poco) 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 cuál los datos "sobrevivirán" cuando codifiquemos en C.

    En assembly, donde codificamos la carga y descarga de registros de 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 código complejo que maneja posibles colisiones.

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

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

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

Si un compilador nota que dos punteros apunte a diferentes tipos (por ejemplo, un int * y un 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 máquina más rápido.

Por ejemplo:

Vamos a asumir 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 los datos de la memoria a los registros de la CPU, por lo que el el código podría terminar así:

  1. Carga a y b desde la memoria.

  2. Añadir a a b.

  3. Guardar b y recarga a.

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

  4. Añadir b a a.

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

El paso 3 es muy lento porque necesita accede a la memoria física. Sin embargo, es necesario proteger contra instancias donde a y b apuntan 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, usando diferentes tipos para punto a. es decir:

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

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

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

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

  1. Carga a y b desde la memoria.

  2. Añadir a a b.

  3. Guardar el resultado tanto en a como en b.

Esta optimización no podría haberse hecho antes, debido a la posible colisión (donde a y b se triplicarían en lugar de duplicarse).

 9
Author: Myst,
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
2018-01-16 14:11:07

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

Este artículo debería ayudarle a entender el problema con todo detalle.

 5
Author: Jason Dagit,
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
2013-11-04 13:38:55

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

Nótese la definición del operador indirection (* ):

El operador unario * realiza indirection: la expresión a la que se aplica debe ser un puntero a un tipo de objeto, o un puntero a un tipo de función y el resultado es un lvalue que se refiere al objeto o función a la 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 objeto, (...snip)

Así que en cualquier traza de programa bien definida, un glvalue se refiere a un objeto. Así que la llamada regla de alias estricta no se aplica, nunca. Esto puede no ser lo que los diseñadores querían.

 -1
Author: curiousguy,
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
2018-07-09 03:24:02