¿Qué es el alias estricto y por qué debería importarnos? Parte 1

(O mecanografía, comportamiento vago y alineación, ¡Dios mío!)

Hola a todos, en unas pocas semanas estamos lanzando un nuevo hilo en el curso "C ++ Developer" . Este evento estará dedicado al material de hoy.

¿Qué es el alias estricto? Primero, describimos qué es el alias, y luego descubrimos para qué sirve la rigurosidad.

En C y C ++, el alias está relacionado con qué tipos de expresiones se nos permite acceder a los valores almacenados. Tanto en C como en C ++, el estándar define qué expresiones de nomenclatura son válidas para qué tipos. El compilador y el optimizador pueden asumir que seguimos estrictamente las reglas de alias, de ahí el término: la regla de alias estricto (regla de alias estricto). Si intentamos acceder a un valor utilizando un tipo no válido, se clasifica como comportamiento indefinido (UB). Cuando tenemos un comportamiento incierto, todas las apuestas se hacen, los resultados de nuestro programa dejan de ser confiables.

Desafortunadamente, con violaciones de alias estrictas, a menudo obtenemos los resultados esperados, dejando la posibilidad de que una versión futura del compilador con una nueva optimización viole el código que consideramos válido. Esto no es deseable, vale la pena entender las estrictas reglas de alias y evitar romperlas.



Para comprender mejor por qué deberíamos estar preocupados por esto, discutiremos los problemas que surgen al violar las estrictas reglas de alias, el tipo de punteo, ya que a menudo se usa en reglas de alias estricto, así como también cómo crear un juego de palabras correctamente, junto con alguna posible ayuda con C ++ 20 para simplificar el juego de palabras y reducir la posibilidad de errores. Resumiremos la discusión considerando algunos métodos para detectar violaciones de reglas estrictas de alias.

Ejemplos preliminares

Echemos un vistazo a algunos ejemplos, y luego podemos discutir lo que se establece exactamente en la (s) norma (s), considerar algunos ejemplos adicionales y luego ver cómo evitar el alias estricto e identificar las violaciones que pasamos por alto. Aquí hay un ejemplo que no debería sorprenderte:

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

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

El siguiente ejemplo muestra alias, lo que conduce a un comportamiento indefinido:

 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 int * y float *. En este ejemplo, llamamos a foo y configuramos 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 el parámetro de plantilla. En este caso, le decimos que procese la expresión & x como si fuera de tipo float *. Podemos esperar ingenuamente que el resultado del segundo cout será 0, pero cuando se habilita la optimización usando -O2 y gcc, y clang obtendrá el siguiente resultado:
0 0
1

Lo cual puede ser inesperado, pero completamente correcto, ya que causamos un comportamiento indefinido. Float no puede ser un alias válido de un objeto int. Por lo tanto, el optimizador puede suponer que la constante 1 almacenada durante la desreferenciación i será el valor de retorno, ya que guardar a través de f no puede afectar correctamente el objeto int. Conectar el código en Compiler Explorer muestra que esto es exactamente lo que sucede ( ejemplo ):

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

Un optimizador que utiliza el Análisis de alias basado en tipo (TBAA) supone que se devolverá 1 y mueve directamente el valor constante al registro eax, que almacena el valor de retorno. TBAA utiliza reglas de lenguaje sobre qué tipos están permitidos para el alias para optimizar la carga y el almacenamiento. En este caso, TBAA sabe que float no puede ser un alias de int, y optimiza la carga hasta la muerte.

Ahora a la referencia

¿Qué dice exactamente el estándar sobre lo 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 significado.

¿Qué dice el estándar C11?

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

El objeto debe tener su propio valor almacenado, cuyo acceso se realiza solo mediante la expresión lvalue, que tiene 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   lvalue-  int,    int 

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

 int x = 1; const int *p = &x; printf("%d\n", *p); // * p   lvalue-  const int,    int 

- un tipo que es un tipo con o sin un signo correspondiente a un tipo calificado de objeto,

 int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  unsigned int,      

Consulte la nota al pie 12 para la extensión gcc / clang , que le permite asignar int * int * sin firmar, incluso si no son tipos compatibles.

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

 int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  const unsigned int,     ,        

- un tipo agregado o combinado que incluye uno de los tipos anteriores entre sus miembros (incluido, recursivamente, un miembro de una asociación subagregada o contenida), o

 struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo -  ,   int   ,       *ip // foo f; foobar( &f, &f.x ); 

- tipo de personaje.

 int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p   lvalue-  char,    . //    -    . 

Lo que dice C ++ 17 Draft Standard

El estándar del proyecto C ++ 17 en la sección 11 [basic.lval] establece: si un programa intenta acceder a un valor almacenado de un objeto a través de un valor gl que no sea uno de los siguientes tipos, el comportamiento es indefinido: 63 (11.1) es un tipo dinámico de objeto,

 void *p = malloc( sizeof(int) ); //   ,       int *ip = new (p) int{0}; // placement new      int std::cout << *ip << "\n"; // * ip   glvalue-  int,       

(11.2) - versión calificada por cv (cv - constante y volátil) del tipo dinámico de un objeto,

 int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip    glvalue  const int,   cv-    x 

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

//

(11.4) - un tipo que es un tipo con o sin un signo correspondiente al tipo dinámico de un objeto,
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .

 signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; } 

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

 signed int foo( const signed int &si1, int &si2); //  ,     

(11.6): un tipo agregado o combinado que incluye uno de los tipos anteriores entre sus elementos o elementos de datos no estáticos (incluido, recursivamente, un elemento o elemento de datos no estático de un subconjunto o asociaciones que contienen),

 struct foo { int x; }; 

// Compiler Explorer (https://godbolt.org/g/z2wJTC)

 int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx ); 

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

 struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; } 

(11.8) - escriba 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   glvalue-  std::byte,      uint32_t } 

Vale la pena señalar que el signed char no signed char incluido en la lista anterior, esta es una diferencia notable de C, que habla sobre el tipo de personaje.

Diferencias sutiles

Por lo tanto, aunque podemos ver que C y C ++ dicen cosas similares sobre el aliasing, hay algunas diferencias que debemos tener en cuenta. C ++ no tiene un concepto C de tipo válido o compatible , y C no tiene un concepto C ++ de tipo dinámico o similar. Aunque ambos tienen expresiones lvalue y rvalue, C ++ también tiene expresiones glvalue, prvalue y xvalue. Estas diferencias están fuera del alcance de este artículo, pero un ejemplo interesante es cómo crear un objeto a partir de la memoria utilizada por malloc. En C, podemos establecer un tipo válido, por ejemplo, escribir en la memoria a través de lvalue o memcpy.

 //     C,    C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); //   *p - float  C //  float *fp = p; *fp = 1.0f; //   *p - float  C 

Ninguno de estos métodos es suficiente en C ++, que requiere la colocación de nuevos:

 float *fp = new (p) float{1.0f} ; //   *p  float 

¿Son los tipos de caracteres int8_t y uint8_t char?

Teóricamente, ni int8_t ni uint8_t deberían ser tipos char, pero en la práctica se implementan de esa manera. Esto es importante porque si realmente son tipos de personajes, también son alias como los tipos de caracteres. Si no es consciente de esto, esto puede conducir a una degradación inesperada del rendimiento . Vemos que glibc typedef int8_t y uint8_t para signed char unsigned char y unsigned char respectivamente.

Esto sería difícil de cambiar, ya que para C ++ sería una brecha ABI. Esto alteraría la distorsión del nombre y rompería cualquier API usando cualquiera de estos tipos en su interfaz.

El final de la primera parte. Y hablaremos sobre el juego de palabras de mecanografía y alineación en unos días.

Escriba sus comentarios y no se pierda el seminario web abierto , que se llevará a cabo el 6 de marzo por el jefe de desarrollo de tecnología en Rambler & Co, Dmitry Shebordaev .

Source: https://habr.com/ru/post/442554/


All Articles