Considere los siguientes tres struct
s:
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 int
hay 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 blub
y blob
, a pesar de que el tamaño 1 blob
podría, en principio, "encajar" en el relleno de blub
.
C ++ 20 introduce el no_unique_address
atributo, 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 bla
gotas disminuye 8
, por lo que blob
se 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::memcpy
de 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 blub
y blob
no trivialmente copiable .
fuente
Respuestas:
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 :
Entonces, la representación del objeto
b0
consiste 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 :
Por lo tanto, la vida útil de
b0
terminaría, cuando el almacenamiento que está ocupado sea reutilizado por otro objeto, es decirb1
. 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 porb1
. 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 quebla
necesita 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)
(é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
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)
¿Qué rastreo estándar estaría completo sin un rastreo de diccionario?
fuente
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".