c++ c++11 language-lawyer unions object-lifetime

c++ - memcpy c



memcpy/memmove a un miembro de la unión, ¿establece esto el miembro ''activo''? (4)

[class.union] / 5:

En una unión, un miembro de datos no estáticos está activo si su nombre se refiere a un objeto cuya vida útil ha comenzado y no ha finalizado ([basic.life]). Como máximo, uno de los miembros de datos no estáticos de un objeto de tipo de unión puede estar activo en cualquier momento, es decir, el valor de como máximo uno de los miembros de datos no estáticos se puede almacenar en una unión en cualquier momento.

A lo sumo, un miembro de un sindicato puede estar activo a la vez.

Un miembro activo es aquel cuya vida ha comenzado y no ha terminado.

Por lo tanto, si finaliza la vida útil de un miembro de su sindicato, ya no estará activo.

Si no tiene miembros activos, el inicio de la vida de otro miembro de la unión está bien definido en el estándar y hace que se active.

El sindicato ha asignado almacenamiento suficiente para todos sus miembros. Todos se asignan como si estuvieran solos, y son interconvertibles de puntero. [class.union]/2 .

[basic.life] / 6

Antes de que la vida útil de un objeto haya comenzado, pero después de que el almacenamiento que ocupará el objeto haya sido asignado 40 o, una vez que haya finalizado la vida útil de un objeto y antes de que el almacenamiento que el objeto ocupado se haya reutilizado o liberado, cualquier puntero que represente la dirección de la ubicación de almacenamiento donde se ubicará o se ubicará el objeto se puede usar, pero solo de manera limitada. Para un objeto en construcción o destrucción, vea [class.cdtor] . De lo contrario, dicho puntero se refiere al almacenamiento asignado ( [basic.stc.dynamic.deallocation] ), y el uso del puntero como si el puntero fuera de tipo void *, está bien definido.

Por lo tanto, puede llevar un puntero a un miembro de la unión y tratarlo como un puntero al almacenamiento asignado. Tal puntero puede usarse para construir un objeto allí, si tal construcción es legal.

La nueva ubicación es una forma válida de construir un objeto allí. memcpy de tipos copiables trivialmente (incluidos los tipos POD) es una forma válida de construir un objeto allí.

Pero, la construcción de un objeto solo es válido si no viola la regla de que haya un miembro activo de la unión .

Si asigna a un miembro de una unión bajo ciertas condiciones [class.union]/6 , primero finaliza la vida útil del miembro actualmente activo, luego comienza la vida útil del miembro asignado. Entonces tu u.u32_in_a_union = 0xaaaabbbb; es legal incluso si hay otro miembro activo en la unión (y hace que u32_in_a_union activo).

Este no es el caso con la ubicación nueva o memcpy , no hay un "tiempo de vida del miembro activo activo" explícito en la especificación de la unión. Debemos buscar en otro lado:

[basic.life] / 5

Un programa puede terminar la vida útil de cualquier objeto reutilizando el almacenamiento que el objeto ocupa o llamando explícitamente al destructor por un objeto de un tipo de clase con un destructor no trivial.

La pregunta es: ¿está comenzando la vida útil de un miembro diferente del sindicato "reutilizando el almacenamiento", y así finalizando la vida útil de los otros miembros del sindicato? En la práctica, obviamente (son interconvertibles a punteros, comparten la misma dirección, etc.). [class.union]/2 .

Así que yo diría que sí.

Así que crear otro objeto a través de un puntero void* (ubicación nueva, o memcpy si es legal para el tipo) finaliza la vida útil de los miembros alternativos de la union (si existe) (no llama a su destructor, pero eso generalmente es correcto), y hace que El objeto apuntado activo y vivo, a la vez.

Es legal comenzar la vida útil de un double o una matriz de int16_t o similar a través de memcpy sobre almacenamiento.

La legalidad de copiar una matriz de dos uint16_t sobre un uint32_t o viceversa dejaré a otros discutir. Aparentemente es legal en C ++ 17. Pero este objeto de ser un sindicato no tiene nada que ver con esa legalidad.

Esta respuesta se basa en la discusión con @Lorehead debajo de su respuesta. Sentí que debería proporcionar una respuesta que apunte directamente a creo que el núcleo del problema.

Aclaración importante: algunos comentaristas parecen pensar que estoy copiando de un sindicato. Mire detenidamente el memcpy , se copia de la dirección de un uint32_t antiguo que no está contenido dentro de una unión. Además, estoy copiando (a través de memcpy ) a un miembro específico de una unión ( u.a16 o &u.x_in_a_union , no directamente a la propia unión completa ( &u )

C ++ es bastante estricto con los sindicatos: debe leer de un miembro solo si ese fue el último miembro al que se escribió:

9.5 Uniones [class.union] [[c ++ 11]] En una unión, como máximo, uno de los miembros de datos no estáticos puede estar activo en cualquier momento, es decir, el valor de uno de los no estáticos Los miembros de los datos se pueden almacenar en una unión en cualquier momento.

(Por supuesto, el compilador no realiza un seguimiento de qué miembro está activo. Es responsabilidad del desarrollador asegurarse de realizar un seguimiento de esto por sí mismos)

Actualización: este bloque de código siguiente es la pregunta principal, reflejando directamente el texto en el título de la pregunta. Si este código está bien, tengo un seguimiento con respecto a otros tipos, pero ahora me doy cuenta de que este primer bloque de código es interesante en sí mismo.

#include <cstdint> uint32_t x = 0x12345678; union { double whatever; uint32_t x_in_a_union; // same type as x } u; u.whatever = 3.14; u.x_in_a_union = x; // surely this is OK, despite involving the inactive member? std::cout << u.x_in_a_union; u.whatever = 3.14; // make the double ''active'' again memcpy(&u.x_in_a_union, &x); // same types, so should be OK? std::cout << u.x_in_a_union; // OK here? What''s the active member?

El bloque de código inmediatamente arriba de este es probablemente el problema principal en los comentarios y respuestas. En retrospectiva, ¡no necesitaba mezclar tipos en esta pregunta! Básicamente, ¿es ua = b lo mismo que memcpy(&u.a,&b, sizeof(b)) , asumiendo que los tipos son idénticos?

Primero, un memcpy relativamente simple que nos permite leer un uint32_t como una matriz de uint16_t :

#include <cstdint> # to ensure we have standard versions of these two types uint32_t x = 0x12345678; uint16_t a16[2]; static_assert(sizeof(x) == sizeof(a16), ""); std:: memcpy(a16, &x, sizeof(x));

El comportamiento preciso depende del carácter endian de su plataforma, y ​​debe tener cuidado con las representaciones de trampas, etc. Pero en general se acuerda aquí (¡creo que se agradece la retroalimentación!) Que, con cuidado de evitar los valores problemáticos, el código anterior puede ser perfectamente una queja de estándares en el contexto correcto en la plataforma correcta.

(Si tiene un problema con el código anterior, comente o edite la pregunta en consecuencia. Quiero estar seguro de que tenemos una versión no controvertida de lo anterior antes de continuar con el código "interesante" a continuación).

Si, y solo si , los dos bloques de código anteriores no son UB, me gustaría combinarlos de la siguiente manera:

uint32_t x = 0x12345678; union { double whatever; uint16_t a16[2]; } u; u.whatever = 3.14; // sets the ''active'' member static_assert(sizeof(u.a16) == sizeof(x)); //any other checks I should do? std:: memcpy(u.a16, &x, sizeof(x)); // what is the ''active member'' of u now, after the memcpy? cout << u.a16[0] << '' '' << u.a16[1] << endl; // i.e. is this OK?

¿Qué miembro del sindicato, u.whatever o u.a16 , es el "miembro activo"?

Finalmente, mi propia suposición es que la razón por la que nos importa esto, en la práctica, es que un compilador de optimización puede no darse cuenta de que el memcpy ocurrió y, por lo tanto, hacer suposiciones falsas (pero suposiciones admisibles, según el estándar) sobre qué miembro está activo y qué tipos de datos están "activos", lo que conduce a errores en torno al alias. El compilador podría reordenar el memcpy de maneras extrañas. ¿Es este un resumen apropiado de por qué nos importa esto?


Como máximo, un miembro de un sindicato puede estar activo, y está activo durante su vida útil.

En la norma C ++ 14 (§ 9.3, o 9.5 en el borrador), todos los miembros de la unión no estática se asignan como si fueran el único miembro de una struct , y comparten la misma dirección. Esto no comienza la vida útil, pero sí lo hace un constructor predeterminado no trivial (que solo puede tener un miembro de la unión). Existe una regla especial que la asignación a un miembro de la unión lo activa, aunque normalmente no se podría hacer esto a un objeto cuya vida no haya comenzado. Si el sindicato es trivial, él y sus miembros no tienen que preocuparse por los destructores no triviales. De lo contrario, debe preocuparse por cuándo finaliza la vida útil del miembro activo. De la norma (§ 3.8.5):

Un programa puede terminar la vida útil de cualquier objeto reutilizando el almacenamiento que el objeto ocupa o llamando explícitamente al destructor por un objeto de un tipo de clase con un destructor no trivial. [... I] f no hay una llamada explícita al destructor o si no se usa una expresión de eliminación para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tiene comportamiento indefinido.

En general, es más seguro llamar explícitamente al destructor del miembro actualmente activo y hacer que otro miembro se active con una new ubicación. El estándar da el ejemplo:

u.m.~M(); new (&u.n) N;

Puede verificar en el momento de la compilación si la primera línea es necesaria con std::is_trivially_destructible . Mediante una lectura estricta de la norma, solo puede comenzar la vida útil de un miembro de la unión inicializando la unión, asignándola o colocándola en un lugar new , pero una vez que lo haya hecho, puede copiar de forma segura un objeto que se pueda copiar de forma trivial sobre otro utilizando memcpy() . (§ 3.9.3, 3.8.8)

Para tipos trivialmente copiables, la representación del valor es un conjunto de bits en la representación del objeto que determina el valor, y la interpretación del objeto de T es una secuencia de objetos de caracteres unsigned char sizeof(T) . La función memcpy() copia esta representación de objeto. Todos los miembros de la unión no estática tienen la misma dirección, y puede usar esa dirección como un void* para el almacenamiento después de que se haya asignado y antes de que comience la vida útil del objeto (§3.8.6), así que puede pasarla a memcpy() cuando el miembro esta inactivo Si la unión es una unión de diseño estándar, la dirección de la misma unión es la misma que la dirección de su primer miembro no estático, y por lo tanto todos ellos. (Si no, es interconvertible con static_cast ).

Si un tipo has_unique_object_representations , se puede copiar de forma trivial, y no hay dos valores distintos que compartan la misma representación de objeto; Es decir, no hay bits de relleno.

Si un tipo is_pod (Datos antiguos), entonces se puede copiar de forma trivial y tiene un diseño estándar, por lo que su dirección también es la misma que la de su primer miembro no estático.

En C , tenemos la garantía de que podemos leer miembros de la unión inactivos de un tipo compatible hasta el último escrito. En C ++ , no lo hacemos. Existen algunos casos especiales en los que funciona, como los punteros que contienen direcciones de objetos del mismo tipo, tipos integrales con signo y sin signo del mismo ancho y estructuras compatibles con el diseño. Sin embargo, los tipos que usó en su ejemplo tienen algunas garantías adicionales: si existen, uint16_t y uint32_t tienen anchos exactos y sin relleno, cada representación de objeto es un valor único, y todos los elementos de la matriz son contiguos en la memoria, por lo que cualquier objeto La representación de uint32_t también es una representación de objeto válida de algunos uint16_t[2] , aunque esta representación de objeto no está definida técnicamente. Lo que obtienes depende de la endianidad. (Si realmente desea dividir 32 bits de forma segura, puede usar desplazamientos de bits y máscaras de bits).

Para generalizar, si el objeto de origen is_pod , entonces puede copiarse estrictamente por su representación de objeto y colocarse sobre otro objeto compatible con el diseño en la nueva dirección, y si el objeto de destino es del mismo tamaño y has_unique_object_representations , se puede copiar de forma trivial bien y no tirará ninguno de los bits, sin embargo, podría haber una representación de trampa. Si su unión no es trivial, debe eliminar el miembro activo (solo un miembro de una unión no trivial puede tener un constructor predeterminado no trivial, y estará activo de forma predeterminada) y usar la ubicación new para hacer que el miembro objetivo activo.

Siempre que copie arrays en C o C ++, siempre desea verificar el desbordamiento del búfer. En este caso, tomaste mi sugerencia y static_assert() . Esto no tiene sobrecarga de tiempo de ejecución. También puede usar memcpy_s() : memcpy_s( &u, sizeof(u), &u32, sizeof(u32) ); funcionará si el origen y el destino son POD (trivialmente copiable con diseño estándar) y si la unión tiene un diseño estándar. Nunca desbordará ni desbordará una unión. Eliminará los bytes restantes de la unión con ceros, lo que puede hacer que muchos de los errores que le preocupan sean visibles y reproducibles.


El elefante en la habitación: las uniones no se admiten en absoluto en C ++ estricto y completo , el "lenguaje" que se obtiene al intentar aplicar todas las cláusulas estándar del intento fallido de formalizar la intuición de C ++ llamada la norma.

Esto es porque:

  • un lvalor se refiere a un objeto,
  • un acceso de miembro ( xm ) es un valor normal para cualquier clase o unión,
  • todos los miembros de una clase o sindicato en vivo pueden ser designados en cualquier momento por un miembro de acceso,
  • de acuerdo con las estrictas reglas de por vida, solo un objeto miembro puede estar vivo en una unión,
  • la noción de un valor l que se refiere a un objeto que se creará pronto no se define en el estándar.

Así que un simple uso de una unión como:

union { char c; int i; } u; u.i = 1;

no tiene un comportamiento definido porque el resultado de la evaluación de ui no puede referirse a ningún objeto int , ya que no existe tal objeto en el momento de la evaluación.

El comité de C ++ falló en su misión .

De hecho, nadie usa C ++ estricto completo para ningún propósito, las personas deben descartar partes completas de la norma o inventar cláusulas imaginarias completas inspiradas en el texto escrito, o volver del texto a la intención que imaginan, y luego volver a formalizar la intención , para darle sentido .

Diferentes personas despiden diferentes partes y terminan con diferentes formalismos diferentes.

Mi propuesta es descartar las reglas de por vida y tener un objeto en cualquier dirección que pueda contener dicho objeto. Eso resuelve todo el problema y nadie ha presentado una objeción válida al enfoque (afirmaciones vagas de que "esto rompe a todos los invariantes" no es una objeción válida). Tener un objeto en cualquier dirección válida simplemente crea un número infinito de objetos potenciales (en particular, todos los tipos de punteros, int* , int** , int*** ...) pero no se pueden usar porque no se ha escrito ningún valor válido .

Tenga en cuenta que sin la relajación de la regla de vida útil o la definición de los valores de l, no puede tener una "regla de alias estricta no trivial" ya que esa regla no se aplicaría a un programa bien definido sin esas reglas. Como se interpreta actualmente, la "regla estricta de aliasing" es inútil. (También está tan mal escrito que nadie sabe lo que significa de todos modos).

O tal vez alguien me diga que para dar sentido a la regla estricta de aliasing, un valor de int refiere a un objeto, solo de un tipo diferente. Eso sería tan sorprendente y tonto que incluso si haces una interpretación coherente del estándar de esa manera, yo diría que está roto.


Mi lectura del estándar es que std::memcpy es seguro siempre que el tipo se pueda copiar de forma trivial .

A partir de las 9 clases, podemos ver que las union son tipos de clase y, por lo tanto, se pueden copiar de forma trivial .

Una unión es una clase definida con la unión clase-clave ; solo tiene un miembro de datos a la vez (9.5).

Una clase trivialmente copiable es una clase que:

  • no tiene constructores de copias no triviales (12.8),
  • no tiene constructores de movimientos no triviales (12.8),
  • no tiene operadores de asignación de copia no triviales (13.5.3, 12.8),
  • no tiene operadores de asignación de movimientos no triviales (13.5.3, 12.8), y
  • Tiene un destructor trivial (12.4).

El significado exacto de trivialmente copiable se da en 3.9 Tipos:

Para cualquier objeto (que no sea un subobjeto de clase base) de tipo T pueda copiar de forma trivial, ya sea que el objeto tenga o no un valor válido de tipo T , los bytes subyacentes (1.7) que forman el objeto se pueden copiar en una matriz de caracteres o unsigned char . Si el contenido de la matriz de caracteres char o unsigned char se copia nuevamente en el objeto, el objeto mantendrá su valor original.

Para cualquier tipo T trivialmente copiable, si dos punteros a T apuntan a objetos T obj1 y obj2 , donde ni obj1 ni obj2 es un subobjeto de clase base, si los bytes subyacentes (1.7) que forman el obj1 se copian en obj2 , obj2 posteriormente mantenga el mismo valor que obj1 .

El estándar también da un ejemplo explícito de ambos.

Entonces, si estuvieras copiando la unión completa, la respuesta sería inequívocamente sí, el miembro activo se "copiará" junto con los datos. (Esto es relevante porque indica que std::memcpy debe considerarse como un medio válido para cambiar el elemento activo de una unión, ya que su uso está explícitamente permitido para la copia de toda la unión).

Ahora, en cambio, estás copiando a un miembro de la unión. El estándar no parece requerir ningún método particular de asignación a un miembro de la unión (y por lo tanto, hacerlo activo). Todo lo que hace es especificar (9.5) que

[Nota: En general, uno debe usar operadores de clase y destructores explícitos para cambiar el miembro activo de una unión. - nota final]

que dice, por supuesto, porque C ++ 11 permite objetos de tipo no trivial en uniones. Tenga en cuenta el "en general" en el frente, que indica claramente que otros métodos para cambiar el miembro activo están permitidos en casos específicos; Ya sabemos que este es el caso porque la asignación está claramente permitida. Ciertamente, no hay ninguna prohibición de usar std::memcpy , donde su uso sería válido de otra manera.

Entonces mi respuesta es sí, esto es seguro, y sí, cambia el miembro activo.