¿El uso de reinterpret_cast en una memoria intermedia de memoria intermedia UB?

8

Dado el código

struct A {};

auto obj = new A;
std::vector<unsigned char> buffer;
buffer.resize(sizeof(obj));
std::memcpy(buffer.data(), &obj, sizeof(obj));  // this copies the pointer, not the object!

// ...

auto ptr = *reinterpret_cast<A**>(buffer.data()); // is this UB?
delete ptr;

Cuál es el uso de reinterpret_casten este caso UB? Yo diría que sí, porque memcpyno comienza la vida útil de una instancia, por lo tanto, viola la estricta regla de alias (por eso std::bit_castse ha agregado a C ++ 20).

Y si reemplazo el elenco con otro memcpy(para leer el puntero), ¿estaría bien definido el programa?

Timo
fuente
dejando de lado la abogacía lingüística, es simplemente incorrecto. Es el contenido señalado por lo buffer.data()que supuestamente contiene un puntero a A, no es a buffer.data()sí mismo a lo que apunta A.
StoryTeller - Unslander Monica
3
¿Hay alguna garantía de alineación de la memoria asignada del almacén de respaldo por a std::vector? (Supongo que sus garantías son cualesquiera que sean las garantías de su asignador.)
Eljay
1
Creo que también rompe el alias estricto.
Algún tipo programador el
1
@anastaciu Primer hit en Google - stats.meta.stackexchange.com/q/5783/3512
Konrad Rudolph
1
OK después de releer la pregunta, de hecho es UB porque (1) los requisitos de alineación están potencialmente rotos y (2) aquí no hay ningún A*objeto en el búfer. El estándar dice acerca de la función allocator :: allocate estándar: "Devuelve: Un puntero al elemento inicial de una matriz de almacenamiento de tamaño n * sizeof(T), alineado apropiadamente para objetos de tipo T ".
n. 'pronombres' m.

Respuestas:

9

Sí, este código tiene un comportamiento indefinido. No hay ningún objeto de tipo A*en la ubicación que señala buffer.data(). Todo lo que hizo fue copiar la representación del objeto de dicho puntero en su vector [basic.types] / 4 . Dado que los punteros se pueden copiar trivialmente [basic.types] / 9 , si copiara estos bytes en un objeto de tipo real A*y luego deleteel valor de eso, eso estaría bien definido [basic.types] / 3 . Así que esto

A* ptr;
std::memcpy(&ptr, buffer.data(), sizeof(ptr));
delete ptr;

estaría bien.

Tenga en cuenta que no es el lanzamiento en sí lo que invoca un comportamiento indefinido en su ejemplo original, sino su intento posterior de leer el valor de un objeto de tipo A*que no existe donde el puntero se obtuvo a través de los puntos de lanzamiento. Todo lo que existe donde el puntero apunta es una secuencia de objetos de tipo unsigned char. El tipo A*no es un tipo que pueda usar para acceder al valor almacenado de un objeto de tipo unsigned char [basic.lval] / 8 ...

Michael Kenzel
fuente
2
Creo que esta respuesta es correcta, pero la última oración es un poco sorprendente, dado que hay una buena cantidad de código existente que hace esencialmente lo que la pregunta hace (prácticamente todas las implementaciones de contenedores personalizados, para empezar), y actualmente no hay realmente una buena evitar esto en algunos casos.
Konrad Rudolph
1
A* a_ptr; std::memcpy(&a_ptr, buffer.data(), sizeof(a_ptr));para extraer el puntero fuera del búfer. En lugar de la reinterpret_castpregunta de Timo.
Eljay
Dejando de lado la abogacía de idiomas, ¿por qué esto es realmente UB? Aliasing estricto viene a la mente, ¿hay algo más?
divinas
@MichaelKenzel hay un objeto de tipo unsigned charallí, tal vez más de uno.
n. 'pronombres' m.
1
@MichaelKenzel Por supuesto que no es defendible, ya que es UB. Pero iniciar la vida útil del objeto es realmente difícil en C ++ (actual), y toneladas de código (¡de otra manera de alta calidad!) No lo hacen correctamente, vea p0593r2 §2.3 en particular, pero también §2.2. Y como mencionas la compresión: eso es realmente lo que hace mi empresa, y digamos que nuestra base de código no presta atención a estos problemas en lo más mínimo (es cierto, ya que gran parte se deriva de la idiomática C).
Konrad Rudolph