studio reales proyectos para libro introducción incluye herramientas fuente español desarrollo código con avanzado aplicaciones c++ strict-aliasing type-punning

c++ - reales - libro de android studio en español pdf



Reutilización de un búfer flotante para dobles sin comportamiento indefinido (6)

En una función de C ++ en particular, tengo un puntero a un gran búfer de flotadores que quiero usar temporalmente para almacenar la mitad del número de dobles. ¿Existe un método para usar este búfer como espacio de memoria virtual para almacenar los dobles, que también está permitido (es decir, no es un comportamiento indefinido) por la norma?

En resumen, me gustaría esto:

void f(float* buffer) { double* d = reinterpret_cast<double*>(buffer); // make use of d d[i] = 1.; // done using d as scratch, start filling the buffer buffer[j] = 1.; }

Por lo que veo, no hay una manera fácil de hacer esto: si entiendo correctamente, un reinterpret_cast<double*> como este provoca un comportamiento indefinido debido al alias de tipo, y usar memcpy o una union flotante / doble no es posible sin copiar los datos y la asignación de espacio adicional, lo que anula el propósito y resulta ser costoso en mi caso (y en C ++ no se permite el uso de una unión para el punning de tipo).

Se puede suponer que el búfer flotante está correctamente alineado para usarlo en dobles.


Aquí hay un enfoque alternativo que da menos miedo.

Tu dices,

... una unión flotante / doble no es posible sin ... asignar espacio adicional, lo que anula el propósito y resulta ser costoso en mi caso ...

Entonces, solo haga que cada objeto de unión contenga dos flotadores en lugar de uno.

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double."); union double_or_floats { double d; float f[2]; }; void f(double_or_floats* buffer) { // Use buffer of doubles as scratch space. buffer[0].d = 1.0; // Done with the scratch space. Start filling the buffer with floats. buffer[0].f[0] = 1.0f; buffer[0].f[1] = 2.0f; }

Por supuesto, esto hace que la indexación sea más complicada, y el código de llamada tendrá que ser modificado. Pero no tiene gastos generales y es más obviamente correcto.


Creo que el siguiente código es una forma válida de hacerlo (en realidad es solo un pequeño ejemplo de la idea):

#include <memory> void f(float* buffer, std::size_t buffer_size_in_bytes) { double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)]; // we have started the lifetime of the doubles. // "d" is a new pointer pointing to the first double object in the array. // now you can use "d" as a double buffer for your calculations // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed" d[0] = 1.; // do some work here on/with the doubles... // conceptually we need to destory the doubles here... but they are trivially destructable // now we need to start the lifetime of the floats again new (buffer) float[10]; // here we are unsure about wether we need to update the "buffer" pointer to // the one returned by the placement new of the floats // if it is nessessary, we could return the new float pointer or take the input pointer // by reference and update it directly in the function } int main() { float* floats = new float[10]; f(floats, sizeof(float) * 10); return 0; }

Es importante que solo use el puntero que recibe de la ubicación nueva. Y es importante colocar de nuevo los flotadores. Incluso si se trata de una construcción sin operación, debe comenzar nuevamente la vida útil de los flotadores.

Olvídate de std::launder y reinterpret_cast en los comentarios. Colocación nueva hará el trabajo por ti.

Editar: asegúrese de tener la alineación correcta al crear el búfer en main.

Actualizar:

Solo quería dar una actualización sobre las cosas que se discutieron en los comentarios.

  1. Lo primero que se mencionó fue que es posible que necesitemos actualizar el puntero flotante creado inicialmente al puntero devuelto por los flotadores re-emplazados nuevos (la pregunta es si el puntero flotante inicialmente todavía se puede usar para acceder a los flotadores, porque los flotadores ahora son flotadores "nuevos" obtenidos por una nueva expresión adicional).

Para hacer esto, podemos a) pasar el puntero flotante por referencia y actualizarlo, o b) devolver el nuevo puntero flotante obtenido de la función:

una)

void f(float*& buffer, std::size_t buffer_size_in_bytes) { double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)]; // do some work here on/with the doubles... buffer = new (buffer) float[10]; }

segundo)

float* f(float* buffer, std::size_t buffer_size_in_bytes) { /* same as inital example... */ return new (buffer) float[10]; } int main() { float* floats = new float[10]; floats = f(floats, sizeof(float) * 10); return 0; }

  1. La siguiente y más importante cosa que mencionar es que la ubicación nueva puede tener una sobrecarga de memoria. Por lo tanto, se permite a la implementación colocar algunos metadatos delante de la matriz devuelta. Si eso sucede, el cálculo ingenuo de cuántos dobles cabrían en nuestra memoria será obviamente incorrecto. El problema es que no sabemos cuántos bytes se cargarán de antemano la implementación para la llamada específica. Pero eso sería necesario para ajustar las cantidades de dobles que sabemos que cabrán en el almacenamiento restante. Aquí ( https://.com/a/8721932/3783662 ) hay otra publicación de SO donde Howard Hinnant proporcionó un fragmento de prueba. Probé esto usando un compilador en línea y vi que para tipos destructivos triviales (por ejemplo, dobles), la sobrecarga era 0. Para los tipos más complejos (por ejemplo, std :: string), había una sobrecarga de 8 bytes. Pero esto puede ser difícil para su plataforma / compilador. Pruébalo de antemano con el fragmento de Howard.

  2. Para la pregunta de por qué necesitamos usar algún tipo de ubicación nueva (ya sea por nuevo [] o por un solo elemento nuevo): se nos permite emitir punteros en todas las formas que queramos. Pero al final, cuando accedemos al valor, debemos utilizar el tipo correcto para evitar la inacción de las estrictas reglas de alias. Fácil de hablar: solo se permite acceder a un objeto cuando realmente hay un objeto del tipo de puntero que vive en la ubicación dada por el puntero. Entonces, ¿cómo traes objetos a la vida? la norma dice:

https://timsong-cpp.github.io/cppwp/intro.object#1 :

"Un objeto es creado por una definición, por una nueva expresión, cuando se cambia implícitamente el miembro activo de una unión, o cuando se crea un objeto temporal".

Hay un sector adicional que puede parecer interesante:

https://timsong-cpp.github.io/cppwp/basic.life#1 :

"Se dice que un objeto tiene una inicialización no vacía si es de una clase o un tipo agregado y uno de sus subobjetos es iniciado por un constructor que no sea un constructor predeterminado trivial. La vida útil de un objeto de tipo T comienza cuando:

  • se obtiene almacenamiento con la alineación y tamaño adecuados para el tipo T, y
  • si el objeto tiene una inicialización no vacía, su inicialización es completa "

Así que ahora podemos argumentar que debido a que los dobles son triviales, ¿debemos tomar alguna acción para dar vida a los objetos triviales y cambiar los objetos vivos reales? Digo que sí, porque inicialmente obtuvimos almacenamiento para los flotadores, y acceder al almacenamiento a través de un doble puntero violaría el aliasing estricto. Así que necesitamos decirle al compilador que el tipo real ha cambiado. Todo este último punto 3 fue bastante controvertido discutido. Puede formarse su propia opinión. Tienes toda la información a la mano ahora.


Este problema no se puede resolver en C ++ portátil.

C ++ es estricto cuando se trata de apuntar aliasing. Algo paradójicamente, esto le permite compilar en muchas plataformas (por ejemplo, donde, tal vez, los números double se almacenan en diferentes lugares para float números).

No hace falta decir que, si te estás esforzando por obtener un código portátil, deberás recodificar lo que tienes. Lo segundo mejor es ser pragmático, aceptar que funcionará en cualquier sistema de escritorio que haya encontrado; tal vez incluso static_assert en nombre / arquitectura del compilador.


Puedes lograr esto de dos maneras.

Primero:

void set(float *buffer, size_t index, double value) { memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double)); } double get(const float *buffer, size_t index) { double v; memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double)); return v; } void f(float *buffer) { // here, use set and get functions }

Segundo: en lugar de float * , debe asignar un búfer char[] "sin tipo", y usar la ubicación nueva para poner flotantes o dobles dentro:

template <typename T> void setType(char *buffer, size_t size) { for (size_t i=0; i<size/sizeof(T); i++) { new(buffer+i*sizeof(T)) T; } } // use it like this: setType<float>(buffer, sizeOfBuffer);

A continuación, utilice este accesorio:

template <typename T> T &get(char *buffer, size_t index) { return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T))); } // use it like this: get<float>(buffer, index) = 33.3f;

Una tercera forma podría ser algo así como la respuesta de phön (vea mis comentarios en esa respuesta), desafortunadamente no puedo encontrar una solución adecuada debido a este problema .


Editar

Pensé en esto un poco más y no se garantiza que sea seguro por las razones que he agregado a mi respuesta original. Así que dejaré el código aquí como referencia, pero no recomiendo que lo use.

En su lugar, haz lo que te sugiero arriba. Es una pena, me gusta bastante el código que escribí, a pesar de los misteriosos downvotes (me supera, pensé que hice un buen trabajo aquí).

Edición 2: (solo para completar, esta publicación ya está muerta)

Esta solución solo funcionará para tipos primitivos y POD. Esto es deliberado, dado el alcance de la pregunta original.

Pensé que publicaría una respuesta de seguimiento porque @ phön ha encontrado una solución mejor diferente que yo y quería aclararlo un poco y aportar algunas ideas por mi cuenta.

Tenga en cuenta: Esta es una publicación seria. El hecho de que me sienta un poco alegre hoy, no significa que solo esté tonteando.

En primer lugar, realmente asignaría el búfer ''maestro'' usando malloc() . Esto es porque:

  • Me devuelve un void * , que en realidad es el apropiado aquí. Ya verás por qué en un minuto.
  • Es un poco más eficiente (aunque esto es un detalle).
  • Puedo controlar la alineación del búfer si lo necesito (para SSE, por ejemplo) con aligned_alloc .

No hay realmente una desventaja en esto. Si quiero administrarlo con un puntero inteligente, siempre puedo usar un borrado personalizado.

Entonces, ¿por qué ese void * tan maravilloso? Porque me impide hacer el tipo de cosas que hizo phön en su publicación , es decir, estuvo tentado de "revertir" el uso del búfer a una serie de float y no creo que sea prudente.

Mejor, más bien, es ciertamente más limpio, usar la ubicación new cada vez que quieras tratar el búfer como una matriz de Foo y luego dejar que el puntero salga silenciosamente fuera del alcance cuando hayas terminado. La sobrecarga es mínima, sin duda para los tipos de POD. De hecho, esperaría que cualquier compilador decente lo optimice completamente en este caso, pero no lo he probado.

Así que, por supuesto, deberías envolver todo esto en una clase, así que vamos a hacer eso. Entonces no necesitamos ese borrador personalizado. Aquí va.

La clase:

#include <cstdlib> #include <new> #include <iostream> class SneakyBuf { public: SneakyBuf (size_t bufsize, size_t alignment = 8) : m_bufsize (bufsize) { m_buf = aligned_alloc (alignment, bufsize); if (m_buf == nullptr) throw std::bad_alloc (); std::cout << std::hex << "m_buf is at " << m_buf << "/n/n"; } ~SneakyBuf () { free (m_buf); } template <class T> T* Cast (size_t& count) { count = m_bufsize / sizeof (T); return new (m_buf) T; // no need for new [] here } private: size_t m_bufsize; void *m_buf; };

Programa de prueba:

void do_float_stuff (SneakyBuf& sb) { size_t count; float *f = sb.Cast <float> (count); std::cout << std::hex << "floats are at " << f << "/n"; std::cout << std::dec << "We have " << count << " floats/n/n"; f [0] = 0; // ... } void do_double_stuff (SneakyBuf& sb) { size_t count; double *d = sb.Cast <double> (count); std::cout << std::hex << "doubles are at " << d << "/n"; std::cout << std::dec << "We have " << count << " doubles/n"; d [0] = 0; // ... } int main () { SneakyBuf sb (100 * sizeof (double)); do_float_stuff (sb); do_double_stuff (sb); }

Salida:

m_buf is at 0x1e56c40 floats are at 0x1e56c40 We have 200 floats doubles are at 0x1e56c40 We have 100 doubles

Demo en vivo .

¡Escrito en mi tableta, trabajo duro!


tl; dr No apunten alias, en absoluto, a menos que le diga al compilador que va a acceder a la línea de comandos.

La forma más fácil de hacer esto podría ser averiguar qué conmutador de compilador desactiva el alias estricto y usarlo para los archivos de origen en cuestión.

Las necesidades deben, ¿eh?

Pensé en esto un poco más. A pesar de todo lo relacionado con la colocación nueva, esta es la única manera segura.

¿Por qué?

Bueno, si tienes dos punteros de tipos diferentes que apuntan a la misma dirección, entonces tienes un alias en esa dirección y tienes una buena posibilidad de engañar al compilador. Y no importa cómo asignó valores a esos punteros . El compilador no va a recordar eso.

Así que esta es la única manera segura, y es por eso que necesitamos std::pun .