“Construir” un objeto copiable trivialmente con memcpy

En C ++, ¿este código es correcto?

#include  #include  struct T // trivially copyable type { int x, y; }; int main() { void *buf = std::malloc( sizeof(T) ); if ( !buf ) return 0; T a{}; std::memcpy(buf, &a, sizeof a); T *b = static_cast(buf); b->x = b->y; free(buf); } 

En otras palabras, ¿es *b un objeto cuya vida ha comenzado? (De ser así, ¿cuándo comenzó exactamente?)

Esto no se ha especificado y es compatible con N3751: duración del objeto, progtwigción de bajo nivel y memcpy, que dice, entre otras cosas:

Los estándares de C ++ actualmente no dicen nada sobre si el uso de memcpy para copiar bytes de representación de objetos es conceptualmente una asignación o una construcción de objeto. La diferencia es importante para las herramientas de transformación y análisis de progtwigs basadas en semántica, así como para los optimizadores, el seguimiento de la duración del objeto. Este artículo sugiere que

  1. Se permitirán los usos de memcpy para copiar los bytes de dos objetos distintos de dos tablas triviales diferentes (pero del mismo tamaño).

  2. tales usos se reconocen como inicialización, o más generalmente como construcción de objetos (conceptualmente).

El reconocimiento como construcción de objetos admitirá IO binario, al tiempo que permite análisis y optimizadores basados ​​en la duración.

No puedo encontrar ninguna minuta de la reunión que tenga este documento discutido, por lo que parece que todavía es un tema abierto.

El borrador del estándar C ++ 14 actualmente dice en 1.8 [intro.object] :

[…] Un objeto es creado por una definición (3.1), por una nueva expresión (5.3.4) o por la implementación (12.2) cuando sea necesario. […]

que no tenemos con el malloc y los casos cubiertos en el estándar para copiar tipos de copia triviales parecen referirse solo a objetos ya existentes en la sección 3.9 [basic.types] :

Para cualquier objeto (que no sea un subobjeto base-clase) de tipo trivialmente copiable T, ya sea que el objeto tenga o no un valor válido de tipo T, los bytes subyacentes (1.7) que componen el objeto pueden copiarse en una matriz de caracteres o unsigned char.42 Si el contenido de la matriz de char o unsigned char se copia de nuevo en el objeto, el objeto conservará posteriormente su valor original […]

y:

Para cualquier tipo Trivially Copyable T, si dos punteros a T apuntan a distintos objetos T obj1 y obj2, donde ni obj1 ni obj2 es un subobjeto de clase base, si los bytes subyacentes (1.7) que componen obj1 se copian en obj2,43 obj2 posteriormente tendrá el mismo valor que obj1. […]

que es básicamente lo que dice la propuesta, por lo que no debería sorprender.

dyp señala una discusión fascinante sobre este tema desde la lista de correo de ub : [ub] Escriba el juego de palabras para evitar copiar .

Propoal p0593: creación implícita de objetos para la manipulación de objetos de bajo nivel

La propuesta p0593 intenta resolver estos problemas, pero AFAIK aún no ha sido revisado.

Este documento propone que los objetos de tipos suficientemente triviales se creen a pedido según sea necesario dentro del almacenamiento recientemente asignado para dar a los progtwigs un comportamiento definido.

Tiene algunos ejemplos motivadores que son de naturaleza similar, incluida una implementación std :: vector actual que actualmente tiene un comportamiento indefinido.

Propone las siguientes formas de crear implícitamente un objeto:

Proponemos que como mínimo se especifiquen las siguientes operaciones como objetos que crean implícitamente:

  • La creación de una matriz de char, unsigned char o std :: byte crea implícitamente objetos dentro de esa matriz.

  • Una llamada a malloc, calloc, realloc o cualquier función llamada operador nuevo u operador nuevo [] crea implícitamente objetos en su almacenamiento devuelto.

  • std :: allocator :: allocate igualmente crea implícitamente objetos en su almacenamiento devuelto; los requisitos del asignador deberían requerir otras implementaciones de asignador para hacer lo mismo.

  • Una llamada a memmove se comporta como si

    • copia el almacenamiento de la fuente a un área temporal

    • implícitamente crea objetos en el almacenamiento de destino, y luego

    • copia el almacenamiento temporal en el almacenamiento de destino.

    Esto permite a memmove preservar los tipos de objetos copiables trivialmente, o ser utilizado para reinterpretar una representación de byte de un objeto como la de otro objeto.

  • Una llamada a memcpy se comporta de la misma manera que una llamada a memmove excepto que introduce una restricción de superposición entre el origen y el destino.

  • Un acceso de miembro de clase que designa a un miembro del sindicato activa la creación de objetos implícitos dentro del almacenamiento ocupado por el miembro del sindicato. Tenga en cuenta que esta no es una regla completamente nueva: este permiso ya existía en [P0137R1] para los casos en que el acceso de miembro está en el lado izquierdo de una asignación, pero ahora se generaliza como parte de este nuevo marco. Como se explica a continuación, esto no permite el tipo de juego de palabras a través de uniones; más bien, simplemente permite que el miembro activo de la unión sea cambiado por una expresión de acceso de miembro de clase.

  • Debe introducirse una nueva operación de barrera (distinta de std :: launder, que no crea objetos) en la biblioteca estándar, con una semántica equivalente a un memmove con el mismo almacenamiento de origen y destino. Como un hombre de paja, sugerimos:

     // Requires: [start, (char*)start + length) denotes a region of allocated // storage that is a subset of the region of storage reachable through start. // Effects: implicitly creates objects within the denoted region. void std::bless(void *start, size_t length); 

Además de lo anterior, se debe especificar un conjunto definido de implementación de funciones de asignación y asignación de memoria no estándar, como mmap en sistemas POSIX y VirtualAlloc en sistemas Windows, como la creación implícita de objetos.

Tenga en cuenta que un puntero reinterpret_cast no se considera suficiente para activar la creación de objetos implícitos.

De una búsqueda rápida .

“… la vida útil comienza cuando el almacenamiento alineado correctamente para el objeto se asigna y finaliza cuando el almacenamiento es desasignado o reutilizado por otro objeto”.

Entonces, diría que con esta definición, la duración comienza con la asignación y finaliza con la gratuita.

Es este código correcto?

Bueno, generalmente “funcionará”, pero solo para tipos triviales.

Sé que no lo solicitó, pero usemos un ejemplo con un tipo no trivial:

 #include  #include  #include  struct T // trivially copyable type { std::string x, y; }; int main() { void *buf = std::malloc( sizeof(T) ); if ( !buf ) return 0; T a{}; ax = "test"; std::memcpy(buf, &a, sizeof a); T *b = static_cast(buf); b->x = b->y; free(buf); } 

Después de construir a , ax se le asigna un valor. Supongamos que std::string no está optimizado para usar un búfer local para valores de cadena pequeños, solo un puntero a un bloque de memoria externo. El memcpy() copia los datos internos de a tal como está en buf . Ahora ax y b->x refieren a la misma dirección de memoria para los datos de string . Cuando b->x tiene asignado un nuevo valor, ese bloque de memoria se libera, pero ax todavía se refiere a él. Cuando a continuación sale del scope al final de main() , intenta liberar el mismo bloque de memoria nuevamente. Comportamiento indefinido ocurre.

Si quiere ser “correcto”, la forma correcta de construir un objeto en un bloque de memoria existente es utilizar el operador de colocación-nuevo , por ejemplo:

 #include  #include  struct T // does not have to be trivially copyable { // any members }; int main() { void *buf = std::malloc( sizeof(T) ); if ( !buf ) return 0; T *b = new(buf) T; // <- placement-new // calls the T() constructor, which in turn calls // all member constructors... // b is a valid self-contained object, // use as needed... b->~T(); // <-- no placement-delete, must call the destructor explicitly free(buf); }