c++ - "Construir" un objeto trivialmente copiable con memcpy
language-lawyer lifetime (3)
¿Es correcto este código?
Bueno, generalmente "funcionará", pero solo para tipos triviales.
Sé que no lo solicitó, pero usemos un ejemplo con un tipo no trivial:
#include <cstdlib>
#include <cstring>
#include <string>
struct T // trivially copyable type
{
std::string x, y;
};
int main()
{
void *buf = std::malloc( sizeof(T) );
if ( !buf ) return 0;
T a{};
a.x = "test";
std::memcpy(buf, &a, sizeof a);
T *b = static_cast<T *>(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 de datos a un bloque de memoria externa.
memcpy()
copia los datos internos de
a
as-is en
buf
.
Ahora
ax
y
b->x
refieren a la misma dirección de memoria para los datos de la
string
.
Cuando a
b->x
se le asigna un nuevo valor, ese bloque de memoria se libera, pero
ax
todavía se refiere a él.
Cuando
a
luego se sale del alcance al final de
main()
, intenta liberar el mismo bloque de memoria nuevamente.
Se produce un comportamiento indefinido.
Si desea ser "correcto", la forma correcta de construir un objeto en un bloque de memoria existente es utilizar el operador de colocación nueva , por ejemplo:
#include <cstdlib>
#include <cstring>
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);
}
En C ++, ¿es correcto este código?
#include <cstdlib>
#include <cstring>
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<T *>(buf);
b->x = b->y;
free(buf);
}
En otras palabras, ¿es
*b
un objeto cuya vida ha comenzado?
(Si es así, ¿cuándo comenzó exactamente?)
De una búsqueda rápida .
"... la vida útil comienza cuando se asigna el almacenamiento alineado correctamente para el objeto y finaliza cuando el almacenamiento es desasignado o reutilizado por otro objeto".
Entonces, diría que según esta definición, la vida comienza con la asignación y termina con la gratuita.
Esto no está especificado, lo que es compatible con N3751: Objeto de por vida, Programación de bajo nivel y memcpy que dice entre otras cosas:
Los estándares de C ++ actualmente no dicen 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 sí importa para las herramientas de análisis y transformación de programas basados en la semántica, así como para los optimizadores, que rastrean la vida útil de los objetos. Este artículo sugiere que
Se permite el uso de memcpy para copiar los bytes de dos objetos distintos de dos tablas copiables triviales diferentes (pero por lo demás del mismo tamaño)
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 vida útil.
No puedo encontrar ningún acta 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 es necesario. [...]
que no tenemos con el
malloc
y los casos cubiertos en el estándar para copiar tipos triviales copiables parecen referirse solo a objetos ya existentes en la sección
3.9
[tipos.básicos]
:
Para cualquier objeto (que no sea un subobjeto de clase base) de tipo T copiable trivialmente, ya sea que el objeto tenga o no un valor válido de tipo T, los bytes subyacentes (1.7) que componen el objeto se pueden copiar 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 posteriormente mantendrá su valor original [...]
y:
Para cualquier tipo T trivialmente copiable, si dos punteros a T apuntan a objetos T distintos 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 ser sorprendente.
dyp señala una discusión fascinante sobre este tema de la lista de correo ub : [ub] Escriba punning 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 se ha 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 programas 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 creación implícita de objetos:
La creación de una matriz de caracteres char, unsigned char o std :: byte crea implícitamente objetos dentro de esa matriz.
Una llamada a malloc, calloc, realloc o cualquier función denominada 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 deben requerir que otras implementaciones del asignador hagan lo mismo.
Una llamada a memmove se comporta como si
copia el almacenamiento de origen en un área temporal
crea implícitamente objetos en el almacenamiento de destino y luego
copia el almacenamiento temporal en el almacenamiento de destino.
Esto permite que memmove conserve los tipos de objetos trivialmente copiables, o que se use para reinterpretar una representación de bytes de un objeto como la de otro objeto.
Una llamada a memcpy se comporta igual 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 nomina a un miembro de unión desencadena la creación de objetos implícitos dentro del almacenamiento ocupado por el miembro de unión. 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 los miembros está en el lado izquierdo de una asignación, pero ahora está generalizado como parte de este nuevo marco. Como se explica a continuación, esto no permite el tipo de castigo a través de los sindicatos; más bien, simplemente permite que el miembro de la unión activa sea cambiado por una expresión de acceso de miembro de clase.
Se debe introducir 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 movimiento de memoria con el mismo almacenamiento de origen y destino. Como 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, 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, debe especificarse como creación implícita de objetos.
Tenga en cuenta que un puntero reinterpret_cast no se considera suficiente para desencadenar la creación de objetos implícitos.