¿Qué evita la superposición de miembros adyacentes en las clases?

12

Considere los siguientes tres structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

En plataformas típicas donde inthay 4 bytes, los tamaños, las alineaciones y el relleno total 1 son los siguientes:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

No hay superposición entre el almacenamiento de los miembros bluby blob, a pesar de que el tamaño 1 blobpodría, en principio, "encajar" en el relleno de blub.

C ++ 20 introduce el no_unique_addressatributo, que permite que los miembros vacíos adyacentes compartan la misma dirección. También permite explícitamente el escenario descrito anteriormente de usar el relleno de un miembro para almacenar otro. De cppreference (énfasis mío):

Indica que este miembro de datos no necesita tener una dirección distinta de todos los demás miembros de datos no estáticos de su clase. Esto significa que si el miembro tiene un tipo vacío (por ejemplo, Asignador sin estado), el compilador puede optimizarlo para que no ocupe espacio, como si fuera una base vacía. Si el miembro no está vacío, cualquier relleno de cola también se puede reutilizar para almacenar otros miembros de datos.

De hecho, si usamos este atributo en blub b0, el tamaño de las blagotas disminuye 8, por lo que blobse almacena en el blub tal como se ve en godbolt .

Finalmente, llegamos a mi pregunta:

¿Qué texto en los estándares (C ++ 11 a C ++ 20) evita esta superposición sin no_unique_address, para los objetos que no se pueden copiar trivialmente?

Necesito excluir objetos trivialmente copiables (TC) de lo anterior, porque para los objetos TC, se permite std::memcpyde un objeto a otro, incluidos los subobjetos de miembros, y si el almacenamiento se superpone, esto se rompería (porque todo o parte del almacenamiento para el miembro adyacente se sobrescribirá) 2 .


1 Calculamos el relleno simplemente como la diferencia entre el tamaño de la estructura y el tamaño de todos sus miembros constituyentes, de forma recursiva.

2 Es por eso que tengo definidos los constructores de copia: para hacer bluby blobno trivialmente copiable .

BeeOnRope
fuente
No he investigado, pero estoy adivinando el "como si" regla. Si no hay una diferencia observable (un término con un significado muy específico por cierto) en la máquina abstracta (que es con lo que se compila su código), entonces el compilador puede cambiar el código como quiera.
Jesper Juhl
Estoy
@JesperJuhl: correcto, pero estoy preguntando por qué no puede hacerlo , no por qué puede hacerlo , y la regla "como si" generalmente se aplica a la primera pero no tiene sentido para la segunda. Además, "como si" no está claro para el diseño de la estructura, que generalmente es una preocupación global, no local. Finalmente, el compilador tiene que tener un conjunto único de reglas consistentes para el diseño, excepto tal vez para las estructuras de las que nunca se puede "escapar".
BeeOnRope
1
@BeeOnRope No puedo responder a tu pregunta, lo siento. Por eso acabo de publicar un comentario y no una respuesta. Lo que obtuviste en ese comentario fue mi mejor suposición hacia una explicación, pero no la respuesta (curioso por aprenderlo yo mismo, por eso obtuviste un voto positivo).
Jesper Juhl
1
@NicolBolas: ¿estás respondiendo a la pregunta correcta? No se trata de detectar copias seguras ni nada más. Más bien tengo curiosidad por qué el relleno no se puede reutilizar entre los miembros. En cualquier caso, está equivocado: la copia trivial es una propiedad del tipo y siempre lo ha sido. Sin embargo, para copiar un objeto de forma segura debe tanto tiene un tipo TC (una propiedad del tipo), y no ser un sujeto potencialmente superpuestas (una propiedad del objeto, que supongo que es donde se confundió). Aún no sé por qué estamos hablando de copias aquí.
BeeOnRope

Respuestas:

1

El estándar es extremadamente silencioso cuando se habla del modelo de memoria y no es muy explícito acerca de algunos de los términos que utiliza. Pero creo que encontré una argumentación funcional (que puede ser un poco débil)

Primero, descubramos qué es incluso parte de un objeto. [tipos.básicos] / 4 :

La representación de objeto de un objeto de tipo Tes la secuencia de N unsigned charobjetos que toma el objeto de tipo T, donde Nes igual sizeof(T). La representación del valor de un objeto de tipo Tes el conjunto de bits que participan en la representación de un valor de tipo T. Los bits en la representación del objeto que no forman parte de la representación del valor son bits de relleno.

Entonces, la representación del objeto b0consiste ensizeof(blub) unsigned char objetos objetos, entonces 8 bytes. Los bits de relleno son parte del objeto.

Ningún objeto puede ocupar el espacio de otro si no está anidado dentro de él [basic.life] /1.5 :

La vida útil de un objeto ode tipo Tfinaliza cuando:

[...]

(1.5) se libera el almacenamiento que ocupa el objeto, o lo reutiliza un objeto que no está anidado en o([intro.object]).

Por lo tanto, la vida útil de b0terminaría, cuando el almacenamiento que está ocupado sea reutilizado por otro objeto, es decir b1. No lo he comprobado, pero creo que el estándar exige que el subobjeto de un objeto que está vivo también debería estar vivo (y no podía imaginar cómo debería funcionar de manera diferente).

Por lo tanto, el almacenamiento que b0 ocupa no puede ser utilizado por b1. No he encontrado ninguna definición de "ocupar" en el estándar, pero creo que una interpretación razonable sería "parte de la representación del objeto". En la cita que describe la representación del objeto, se usan las palabras "tomar" 1 . Aquí, esto sería 8 bytes, por lo que blanecesita al menos uno más parab1 .

Especialmente para los subobjetos (por lo tanto, entre otros miembros de datos no estáticos) también existe la estipulación [intro.object] / 9 (pero esto se agregó con C ++ 20, thx @BeeOnRope)

Dos objetos con vidas superpuestas que no son campos de bits pueden tener la misma dirección si uno está anidado dentro del otro, o si al menos uno es un subobjeto de tamaño cero y son de diferentes tipos; de lo contrario, tienen direcciones distintas y ocupan bytes de almacenamiento disjuntos .

(énfasis mío) Aquí nuevamente, tenemos el problema de que "ocupa" no está definido y nuevamente argumentaría que tomaría los bytes en la representación del objeto. Tenga en cuenta que hay una nota al pie de esta [basic.memobj] / nota al pie 29

Según la regla "como si", una implementación puede almacenar dos objetos en la misma dirección de máquina o no almacenar ningún objeto si el programa no puede observar la diferencia ([intro.execution]).

Lo que puede permitir que el compilador rompa esto si puede probar que no hay efectos secundarios observables. Creo que esto es bastante complicado para algo tan fundamental como el diseño de objetos. Quizás es por eso que esta optimización solo se toma cuando el usuario proporciona la información de que no hay razón para tener objetos disjuntos al agregar[no_unique_address] atributo.

tl; dr: El relleno puede ser parte del objeto y los miembros deben ser disjuntos.


1 No pude resistirme a agregar una referencia que ocupar puede significar tomar: Diccionario Revisado de Webster, G & C. Merriam, 1913 (énfasis mío)

  1. Para mantener, o llenar, las dimensiones de; ocupar la habitación o espacio de; para cubrir o llenar; como, el campamento ocupa cinco acres de tierra. Sir J. Herschel.

¿Qué rastreo estándar estaría completo sin un rastreo de diccionario?

n314159
fuente
2
Creo que la parte "ocupar bytes de almacenamiento disjuntos" de into.storage sería suficiente para mí, pero esta redacción solo se agregó en C ++ 20 como parte del cambio que agregó no_unique_address. Deja la situación anterior a C ++ 20 menos clara. No entendí su razonamiento que conduce a "Ningún objeto puede ocupar el espacio de otro si no está anidado dentro de él" de basic.life/1.5, en particular cómo obtener "el almacenamiento que ocupa el objeto se libera" a "ningún objeto puede ocupar el espacio de otro".
BeeOnRope
1
Agregué una pequeña aclaración a ese párrafo. Espero que eso lo haga más comprensible. De lo contrario, lo volveré a ver mañana, ahora es muy tarde para mí.
n314159
"Dos objetos con vidas superpuestas que no son campos de bits pueden tener la misma dirección si uno está anidado dentro del otro, o si al menos uno es un subobjeto de tamaño cero y son de diferentes tipos" 2 objetos con vidas superpuestas, de del mismo tipo, tienen la misma dirección .
Abogado de idiomas el
Lo siento, ¿podrías explicarlo? Citas una cita estándar de mi respuesta y traes un ejemplo que está un poco en conflicto con eso. No estoy seguro de si este es un comentario sobre mi respuesta y si es lo que debería decirme. Con respecto a su ejemplo, diría que uno tenía que considerar otras partes del estándar (hay un párrafo sobre una matriz de caracteres sin signo que proporciona almacenamiento para otro objeto, algo relacionado con la optimización de base de tamaño cero y aún más uno también debe mirar si la ubicación nueva tiene asignaciones especiales, todas las cosas que no creo que sean relevantes para el ejemplo de OP)
n314159
@ n314159 Creo que esta redacción podría ser defectuosa.
Abogado de idiomas el