c++ c++11 unions strict-aliasing type-punning

c++ - alineación estricta y alineación



c++11 unions (4)

Necesito una forma segura de establecer un alias entre los tipos de POD arbitrarios, conforme a ISO-C ++ 11, considerando explícitamente 3.10 / 10 y 3.11 de n3242 o posterior. Aquí hay muchas preguntas acerca del alias estricto, la mayoría de ellas con respecto a C y no a C ++. Encontré una "solución" para C que usa uniones, probablemente usando esta sección

tipo de unión que incluye uno de los tipos mencionados anteriormente entre sus elementos o miembros de datos no estáticos

De eso construí esto.

#include <iostream> template <typename T, typename U> T& access_as(U* p) { union dummy_union { U dummy; T destination; }; dummy_union* u = (dummy_union*)p; return u->destination; } struct test { short s; int i; }; int main() { int buf[2]; static_assert(sizeof(buf) >= sizeof(double), ""); static_assert(sizeof(buf) >= sizeof(test), ""); access_as<double>(buf) = 42.1337; std::cout << access_as<double>(buf) << ''/n''; access_as<test>(buf).s = 42; access_as<test>(buf).i = 1234; std::cout << access_as<test>(buf).s << ''/n''; std::cout << access_as<test>(buf).i << ''/n''; }

Mi pregunta es, para estar seguros, ¿este programa es legal de acuerdo con la norma? *

No da ninguna advertencia y funciona bien cuando se compila con MinGW / GCC 4.6.2 usando:

g++ -std=c++0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp

* Edición: Y si no, ¿cómo se puede modificar esto para que sea legal?


Mi pregunta es, para estar seguros, ¿este programa es legal de acuerdo con la norma?

No. La alineación puede ser poco natural utilizando el alias que ha proporcionado. La unión que escribiste solo mueve el punto del alias. Puede parecer que funciona, pero ese programa puede fallar cuando cambian las opciones de la CPU, ABI o la configuración del compilador.

Y si no, ¿cómo se puede modificar esto para que sea legal?

Cree variables temporales naturales y trate su almacenamiento como un blob de memoria (moviéndose dentro y fuera del blob a / desde temporales), o use una unión que represente todos sus tipos (recuerde, un elemento activo a la vez aquí).


Aparte del error cuando sizeof(T) > sizeof(U) , el problema podría ser, que la unión tiene una alineación apropiada y posiblemente más alta que U , debido a T Si no crea una instancia de esta unión, de modo que su bloque de memoria se alinee (y sea lo suficientemente grande) y luego busque el miembro con el tipo de destino T , se romperá en silencio en el peor de los casos.

Por ejemplo, se produce un error de alineación, si realiza la conversión de estilo C de U* , donde U requiere alineación de 4 bytes, a dummy_union* , donde dummy_union requiere alineación a 8 bytes, porque alignof(T) == 8 . Después de eso, posiblemente lea el miembro de la unión con el tipo T alineado en 4 en lugar de 8 bytes.

Alias ​​emitidos (alineación y tamaño seguros reinterpret_cast para POD solamente):

Esta propuesta viola explícitamente el aliasing estricto, pero con aseveraciones estáticas:

///@brief Compile time checked reinterpret_cast where destAlign <= srcAlign && destSize <= srcSize template<typename _TargetPtrType, typename _ArgType> inline _TargetPtrType alias_cast(_ArgType* const ptr) { //assert argument alignment at runtime in debug builds assert(uintptr_t(ptr) % alignof(_ArgType) == 0); typedef typename std::tr1::remove_pointer<_TargetPtrType>::type target_type; static_assert(std::tr1::is_pointer<_TargetPtrType>::value && std::tr1::is_pod<target_type>::value, "Target type must be a pointer to POD"); static_assert(std::tr1::is_pod<_ArgType>::value, "Argument must point to POD"); static_assert(std::tr1::is_const<_ArgType>::value ? std::tr1::is_const<target_type>::value : true, "const argument must be cast to const target type"); static_assert(alignof(_ArgType) % alignof(target_type) == 0, "Target alignment must be <= source alignment"); static_assert(sizeof(_ArgType) >= sizeof(target_type), "Target size must be <= source size"); //reinterpret cast doesn''t remove a const qualifier either return reinterpret_cast<_TargetPtrType>(ptr); }

Uso con un argumento de tipo puntero (como operadores de conversión estándar como reinterpret_cast):

int* x = alias_cast<int*>(any_ptr);

Otro enfoque (elude los problemas de alineación y alias mediante una unión temporal):

template<typename ReturnType, typename ArgType> inline ReturnType alias_value(const ArgType& x) { //test argument alignment at runtime in debug builds assert(uintptr_t(&x) % alignof(ArgType) == 0); static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can''t be a const value type"); static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD"); static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type"); //assure, that we don''t read garbage static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size"); union dummy_union { ArgType x; ReturnType r; }; dummy_union dummy; dummy.x = x; return dummy.r; }

Uso:

struct characters { char c[5]; }; //..... characters chars; chars.c[0] = ''a''; chars.c[1] = ''b''; chars.c[2] = ''c''; chars.c[3] = ''d''; chars.c[4] = ''/0''; int r = alias_value<int>(chars);

La desventaja de esto es que la unión puede requerir más memoria de la que realmente se necesita para el ReturnType

Memcpy envuelto (evita problemas de alineación y alias utilizando memcpy):

template<typename ReturnType, typename ArgType> inline ReturnType alias_value(const ArgType& x) { //assert argument alignment at runtime in debug builds assert(uintptr_t(&x) % alignof(ArgType) == 0); static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can''t be a const value type"); static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD"); static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type"); //assure, that we don''t read garbage static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size"); ReturnType r; memcpy(&r,&x,sizeof(ReturnType)); return r; }

Para matrices de tamaño dinámico de cualquier tipo de POD:

template<typename ReturnType, typename ElementType> ReturnType alias_value(const ElementType* const array,const size_t size) { //assert argument alignment at runtime in debug builds assert(uintptr_t(array) % alignof(ElementType) == 0); static const size_t min_element_count = (sizeof(ReturnType) / sizeof(ElementType)) + (sizeof(ReturnType) % sizeof(ElementType) != 0 ? 1 : 0); static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can''t be a const value type"); static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD"); static_assert(std::tr1::is_pod<ElementType>::value, "Array elements must be of POD type"); //check for minimum element count in array if(size < min_element_count) throw std::invalid_argument("insufficient array size"); ReturnType r; memcpy(&r,array,sizeof(ReturnType)); return r; }

Los enfoques más eficientes pueden hacer lecturas no alineadas explícitas con intrínsecas, como las de SSE, para extraer primitivas.

Ejemplos:

struct sample_struct { char c[4]; int _aligner; }; int test(void) { const sample_struct constPOD = {}; sample_struct pod = {}; const char* str = "abcd"; const int* constIntPtr = alias_cast<const int*>(&constPOD); void* voidPtr = alias_value<void*>(pod); int intValue = alias_value<int>(str,strlen(str)); return 0; }

EDICIONES:

  • Las afirmaciones para asegurar la conversión de POD solamente, pueden ser mejoradas.
  • Se eliminaron los ayudantes de plantillas superfluos, ahora solo se utilizan rasgos tr1
  • Afirmaciones estáticas para aclarar y prohibir el tipo de retorno de valor const (no puntero)
  • Aserciones en tiempo de ejecución para construcciones de depuración
  • Se han añadido calificadores const a algunos argumentos de función.
  • Otro tipo de función de punning usando memcpy
  • Refactorización
  • Pequeño ejemplo

Creo que en el nivel más fundamental, esto es imposible y viola el aliasing estricto. Lo único que has logrado es engañar al compilador para que no lo note.


Esto nunca será legal, no importa qué tipo de contorsiones realice con modelos y sindicatos extraños y todo eso.

El hecho fundamental es este: dos objetos de diferente tipo nunca pueden tener un alias en la memoria, con algunas excepciones especiales (ver más abajo).

Ejemplo

Considere el siguiente código:

void sum(double& out, float* in, int count) { for(int i = 0; i < count; ++i) { out += *in++; } }

Vamos a dividirlo en variables de registro local para modelar la ejecución real más de cerca:

void sum(double& out, float* in, int count) { for(int i = 0; i < count; ++i) { register double out_val = out; // (1) register double in_val = *in; // (2) register double tmp = out_val + in_val; out = tmp; // (3) in++; } }

Supongamos que (1), (2) y (3) representan una memoria de lectura, lectura y escritura, respectivamente, que pueden ser operaciones muy costosas en un bucle interno tan cerrado. Una optimización razonable para este bucle sería la siguiente:

void sum(double& out, float* in, int count) { register double tmp = out; // (1) for(int i = 0; i < count; ++i) { register double in_val = *in; // (2) tmp = tmp + in_val; in++; } out = tmp; // (3) }

Esta optimización reduce la cantidad de lecturas de memoria necesarias a la mitad y la cantidad de escrituras de memoria en 1. Esto puede tener un gran impacto en el rendimiento del código y es una optimización muy importante para todos los compiladores C y C ++ que optimizan.

Ahora, supongamos que no tenemos un alias estricto. Supongamos que una escritura en un objeto de cualquier tipo puede afectar a cualquier otro objeto. Supongamos que escribir en un doble puede afectar el valor de un flotador en algún lugar. Esto hace que la optimización anterior sea sospechosa, porque es posible que el programador, de hecho, esté destinado a enviarse al alias para que el resultado de la función de suma sea más complicado y se vea afectado por el proceso. ¿Suena estúpido? Aun así, el compilador no puede distinguir entre el código "estúpido" y el código "inteligente". El compilador solo puede distinguir entre código bien formado y mal formado. Si permitimos un alias libre, el compilador debe ser conservador en sus optimizaciones y debe realizar el almacenamiento adicional (3) en cada iteración del bucle.

Esperemos que pueda ver ahora por qué tal unión o truco de reparto no puede ser legal. No puedes sortear conceptos fundamentales como este con un juego de manos.

Excepciones al aliasing estricto.

Los estándares C y C ++ hacen una disposición especial para crear alias de cualquier tipo con char , y con cualquier "tipo relacionado" que, entre otros, incluya los tipos derivados y base, y los miembros, porque es tan importante poder usar la dirección de un miembro de la clase de manera independiente. Puede encontrar una lista exhaustiva de estas disposiciones en esta respuesta.

Además, GCC hace una disposición especial para leer de un miembro diferente de un sindicato de lo que fue escrito por última vez. Tenga en cuenta que este tipo de conversión a través de la unión no le permite, de hecho, violar el aliasing. Solo un miembro de un sindicato puede estar activo a la vez, así que, por ejemplo, incluso con GCC, el siguiente sería un comportamiento indefinido:

union { double d; float f[2]; }; f[0] = 3.0f; f[1] = 5.0f; sum(d, f, 2); // UB: attempt to treat two members of // a union as simultaneously active

Soluciones

La única forma estándar de reinterpretar los bits de un objeto como los bits de un objeto de otro tipo es usar un equivalente de memcpy . Esto hace uso de la disposición especial para crear alias con objetos char , que le permite leer y modificar la representación del objeto subyacente en el nivel de bytes. Por ejemplo, lo siguiente es legal y no viola las reglas estrictas de alias:

int a[2]; double d; static_assert(sizeof(a) == sizeof(d)); memcpy(a, &d, sizeof(d));

Esto es semánticamente equivalente al siguiente código:

int a[2]; double d; static_assert(sizeof(a) == sizeof(d)); for(size_t i = 0; i < sizeof(a); ++i) ((char*)a)[i] = ((char*)&d)[i];

GCC hace una provisión para la lectura de un miembro de la unión inactiva, haciéndolo implícitamente activo. De la documentación del GCC:

La práctica de leer de un miembro de la unión diferente a la que se escribió más recientemente (llamado "tipificación de tipo") es común. Incluso con -fstrict-aliasing, se permite la tipificación de tipos, siempre que se acceda a la memoria a través del tipo de unión. Por lo tanto, el código anterior funcionará como se espera. Ver enumeración de uniones estructurales e implementación de campos de bits. Sin embargo, este código podría no:

int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }

De manera similar, el acceso al tomar la dirección, lanzar el puntero resultante y eliminar la referencia al resultado tiene un comportamiento indefinido, incluso si la conversión utiliza un tipo de unión, por ejemplo:

int f() { double d = 3.0; return ((union a_union *) &d)->i; }

Colocación nueva

(Nota: Me voy de memoria aquí porque no tengo acceso al estándar en este momento). Una vez que coloca un nuevo objeto en un búfer de almacenamiento, la vida útil de los objetos de almacenamiento subyacentes termina implícitamente. Esto es similar a lo que sucede cuando escribe a un miembro de un sindicato:

union { int i; float f; } u; // No member of u is active. Neither i nor f refer to an lvalue of any type. u.i = 5; // The member u.i is now active, and there exists an lvalue (object) // of type int with the value 5. No float object exists. u.f = 5.0f; // The member u.i is no longer active, // as its lifetime has ended with the assignment. // The member u.f is now active, and there exists an lvalue (object) // of type float with the value 5.0f. No int object exists.

Ahora, veamos algo similar con la colocación nueva:

#define MAX_(x, y) ((x) > (y) ? (x) : (y)) // new returns suitably aligned memory char* buffer = new char[MAX_(sizeof(int), sizeof(float))]; // Currently, only char objects exist in the buffer. new (buffer) int(5); // An object of type int has been constructed in the memory pointed to by buffer, // implicitly ending the lifetime of the underlying storage objects. new (buffer) float(5.0f); // An object of type int has been constructed in the memory pointed to by buffer, // implicitly ending the lifetime of the int object that previously occupied the same memory.

Este tipo de fin de vida implícito solo puede ocurrir para tipos con constructores y destructores triviales, por razones obvias.